From 3e32a9fcbf37ba3c56fac2de1479365630b0ad2e Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Fri, 30 Aug 2024 19:56:17 +0200 Subject: [PATCH 01/37] frontend: Add new frontend directory to gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c1082426..d108e96e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ !/cmake !/deps !/docs +!/frontend !/libobs* !/plugins !/shared From 3bbda4803e3006ca0b649a943cecee2d78b8f3f9 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Sat, 30 Nov 2024 16:41:38 +0100 Subject: [PATCH 02/37] frontend: Add renamed Qt UI components This commit only contains Qt UI components that are self-contained, i.e. the translation units only contain code for a single class or interface and don't mix implementations. --- .../components/AbsoluteSlider.cpp | 3 ++- .../components/AbsoluteSlider.hpp | 1 - .../components/BalanceSlider.hpp | 2 +- .../components/ClickableLabel.hpp | 0 .../components/FocusList.cpp | 5 ++++- .../components/FocusList.hpp | 2 -- .../components/HScrollArea.cpp | 5 ++++- .../components/HScrollArea.hpp | 0 .../components/MediaControls.cpp | 14 ++++++------- .../components/MediaControls.hpp | 7 +++---- .../components/MenuButton.cpp | 6 ++++-- .../components/MenuButton.hpp | 0 .../components/Multiview.cpp | 11 +++++----- .../components/Multiview.hpp | 1 + .../components/MuteCheckBox.hpp | 0 .../components/NonCheckableButton.hpp | 2 ++ .../components/OBSAdvAudioCtrl.cpp | 20 +++++++++---------- .../components/OBSAdvAudioCtrl.hpp | 16 ++++++++------- .../components/OBSSourceLabel.cpp | 3 ++- .../components/OBSSourceLabel.hpp | 3 ++- .../components/SceneTree.cpp | 8 +++----- .../components/SceneTree.hpp | 5 +++-- .../components/UIValidation.cpp | 9 ++++----- .../components/UIValidation.hpp | 5 ++--- .../components/UrlPushButton.cpp | 6 +++--- .../components/UrlPushButton.hpp | 2 ++ 26 files changed, 74 insertions(+), 62 deletions(-) rename UI/absolute-slider.cpp => frontend/components/AbsoluteSlider.cpp (97%) rename UI/absolute-slider.hpp => frontend/components/AbsoluteSlider.hpp (96%) rename UI/balance-slider.hpp => frontend/components/BalanceSlider.hpp (100%) rename UI/clickable-label.hpp => frontend/components/ClickableLabel.hpp (100%) rename UI/focus-list.cpp => frontend/components/FocusList.cpp (89%) rename UI/focus-list.hpp => frontend/components/FocusList.hpp (92%) rename UI/horizontal-scroll-area.cpp => frontend/components/HScrollArea.cpp (75%) rename UI/horizontal-scroll-area.hpp => frontend/components/HScrollArea.hpp (100%) rename UI/media-controls.cpp => frontend/components/MediaControls.cpp (99%) rename UI/media-controls.hpp => frontend/components/MediaControls.hpp (97%) rename UI/menu-button.cpp => frontend/components/MenuButton.cpp (90%) rename UI/menu-button.hpp => frontend/components/MenuButton.hpp (100%) rename UI/multiview.cpp => frontend/components/Multiview.cpp (99%) rename UI/multiview.hpp => frontend/components/Multiview.hpp (99%) rename UI/mute-checkbox.hpp => frontend/components/MuteCheckBox.hpp (100%) rename UI/noncheckable-button.hpp => frontend/components/NonCheckableButton.hpp (92%) rename UI/adv-audio-control.cpp => frontend/components/OBSAdvAudioCtrl.cpp (98%) rename UI/adv-audio-control.hpp => frontend/components/OBSAdvAudioCtrl.hpp (96%) rename UI/source-label.cpp => frontend/components/OBSSourceLabel.cpp (95%) rename UI/source-label.hpp => frontend/components/OBSSourceLabel.hpp (99%) rename UI/scene-tree.cpp => frontend/components/SceneTree.cpp (98%) rename UI/scene-tree.hpp => frontend/components/SceneTree.hpp (95%) rename UI/ui-validation.cpp => frontend/components/UIValidation.cpp (96%) rename UI/ui-validation.hpp => frontend/components/UIValidation.hpp (97%) rename UI/url-push-button.cpp => frontend/components/UrlPushButton.cpp (83%) rename UI/url-push-button.hpp => frontend/components/UrlPushButton.hpp (90%) diff --git a/UI/absolute-slider.cpp b/frontend/components/AbsoluteSlider.cpp similarity index 97% rename from UI/absolute-slider.cpp rename to frontend/components/AbsoluteSlider.cpp index 8aedba825..9f2e76656 100644 --- a/UI/absolute-slider.cpp +++ b/frontend/components/AbsoluteSlider.cpp @@ -1,4 +1,5 @@ -#include "moc_absolute-slider.cpp" +#include "AbsoluteSlider.hpp" +#include "moc_AbsoluteSlider.cpp" AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent) { diff --git a/UI/absolute-slider.hpp b/frontend/components/AbsoluteSlider.hpp similarity index 96% rename from UI/absolute-slider.hpp rename to frontend/components/AbsoluteSlider.hpp index 67e0adb1f..8345037fe 100644 --- a/UI/absolute-slider.hpp +++ b/frontend/components/AbsoluteSlider.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include class AbsoluteSlider : public SliderIgnoreScroll { diff --git a/UI/balance-slider.hpp b/frontend/components/BalanceSlider.hpp similarity index 100% rename from UI/balance-slider.hpp rename to frontend/components/BalanceSlider.hpp index 06be1b5f8..780eb6310 100644 --- a/UI/balance-slider.hpp +++ b/frontend/components/BalanceSlider.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include class BalanceSlider : public QSlider { Q_OBJECT diff --git a/UI/clickable-label.hpp b/frontend/components/ClickableLabel.hpp similarity index 100% rename from UI/clickable-label.hpp rename to frontend/components/ClickableLabel.hpp diff --git a/UI/focus-list.cpp b/frontend/components/FocusList.cpp similarity index 89% rename from UI/focus-list.cpp rename to frontend/components/FocusList.cpp index d5148b3a1..ea65cf207 100644 --- a/UI/focus-list.cpp +++ b/frontend/components/FocusList.cpp @@ -1,6 +1,9 @@ -#include "moc_focus-list.cpp" +#include "FocusList.hpp" + #include +#include "moc_FocusList.cpp" + FocusList::FocusList(QWidget *parent) : QListWidget(parent) {} void FocusList::focusInEvent(QFocusEvent *event) diff --git a/UI/focus-list.hpp b/frontend/components/FocusList.hpp similarity index 92% rename from UI/focus-list.hpp rename to frontend/components/FocusList.hpp index 370dcbcc1..83c44419c 100644 --- a/UI/focus-list.hpp +++ b/frontend/components/FocusList.hpp @@ -2,8 +2,6 @@ #include -class QDragMoveEvent; - class FocusList : public QListWidget { Q_OBJECT diff --git a/UI/horizontal-scroll-area.cpp b/frontend/components/HScrollArea.cpp similarity index 75% rename from UI/horizontal-scroll-area.cpp rename to frontend/components/HScrollArea.cpp index 82ffc0d28..01d804dca 100644 --- a/UI/horizontal-scroll-area.cpp +++ b/frontend/components/HScrollArea.cpp @@ -1,5 +1,8 @@ +#include "HScrollArea.hpp" + #include -#include "moc_horizontal-scroll-area.cpp" + +#include "moc_HScrollArea.cpp" void HScrollArea::resizeEvent(QResizeEvent *event) { diff --git a/UI/horizontal-scroll-area.hpp b/frontend/components/HScrollArea.hpp similarity index 100% rename from UI/horizontal-scroll-area.hpp rename to frontend/components/HScrollArea.hpp diff --git a/UI/media-controls.cpp b/frontend/components/MediaControls.cpp similarity index 99% rename from UI/media-controls.cpp rename to frontend/components/MediaControls.cpp index df720cc3c..d19951dca 100644 --- a/UI/media-controls.cpp +++ b/frontend/components/MediaControls.cpp @@ -1,12 +1,12 @@ -#include "window-basic-main.hpp" -#include "moc_media-controls.cpp" -#include "obs-app.hpp" -#include -#include -#include - +#include "MediaControls.hpp" #include "ui_media-controls.h" +#include + +#include + +#include "moc_MediaControls.cpp" + void MediaControls::OBSMediaStopped(void *data, calldata_t *) { MediaControls *media = static_cast(data); diff --git a/UI/media-controls.hpp b/frontend/components/MediaControls.hpp similarity index 97% rename from UI/media-controls.hpp rename to frontend/components/MediaControls.hpp index 6a5ec5953..5a246facc 100644 --- a/UI/media-controls.hpp +++ b/frontend/components/MediaControls.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include -#include #include -#include + +#include +#include class Ui_MediaControls; diff --git a/UI/menu-button.cpp b/frontend/components/MenuButton.cpp similarity index 90% rename from UI/menu-button.cpp rename to frontend/components/MenuButton.cpp index 8c22efcfa..b67459563 100644 --- a/UI/menu-button.cpp +++ b/frontend/components/MenuButton.cpp @@ -1,7 +1,9 @@ -#include +#include "MenuButton.hpp" + #include #include -#include "moc_menu-button.cpp" + +#include "moc_MenuButton.cpp" void MenuButton::keyPressEvent(QKeyEvent *event) { diff --git a/UI/menu-button.hpp b/frontend/components/MenuButton.hpp similarity index 100% rename from UI/menu-button.hpp rename to frontend/components/MenuButton.hpp diff --git a/UI/multiview.cpp b/frontend/components/Multiview.cpp similarity index 99% rename from UI/multiview.cpp rename to frontend/components/Multiview.cpp index 9fc8883dc..c32c49377 100644 --- a/UI/multiview.cpp +++ b/frontend/components/Multiview.cpp @@ -1,8 +1,9 @@ -#include "multiview.hpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "display-helpers.hpp" +#include "Multiview.hpp" + +#include +#include + +#include Multiview::Multiview() { diff --git a/UI/multiview.hpp b/frontend/components/Multiview.hpp similarity index 99% rename from UI/multiview.hpp rename to frontend/components/Multiview.hpp index 61abd690b..5c62ee5b3 100644 --- a/UI/multiview.hpp +++ b/frontend/components/Multiview.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include enum class MultiviewLayout : uint8_t { diff --git a/UI/mute-checkbox.hpp b/frontend/components/MuteCheckBox.hpp similarity index 100% rename from UI/mute-checkbox.hpp rename to frontend/components/MuteCheckBox.hpp diff --git a/UI/noncheckable-button.hpp b/frontend/components/NonCheckableButton.hpp similarity index 92% rename from UI/noncheckable-button.hpp rename to frontend/components/NonCheckableButton.hpp index d5ef59a37..5892e996c 100644 --- a/UI/noncheckable-button.hpp +++ b/frontend/components/NonCheckableButton.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include /* Button with its checked property not changed when clicked. * Meant to be used in situations where manually changing the property diff --git a/UI/adv-audio-control.cpp b/frontend/components/OBSAdvAudioCtrl.cpp similarity index 98% rename from UI/adv-audio-control.cpp rename to frontend/components/OBSAdvAudioCtrl.cpp index 80c0a5c1c..01aa2e727 100644 --- a/UI/adv-audio-control.cpp +++ b/frontend/components/OBSAdvAudioCtrl.cpp @@ -1,14 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include +#include "OBSAdvAudioCtrl.hpp" + +#include +#include + #include -#include "obs-app.hpp" -#include "moc_adv-audio-control.cpp" -#include "window-basic-main.hpp" + +#include + +#include "moc_OBSAdvAudioCtrl.cpp" #ifndef NSEC_PER_MSEC #define NSEC_PER_MSEC 1000000 @@ -16,6 +15,7 @@ #define MIN_DB -96.0 #define MAX_DB 26.0 + static inline void setMixer(obs_source_t *source, const int mixerIdx, const bool checked); OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) : source(source_) diff --git a/UI/adv-audio-control.hpp b/frontend/components/OBSAdvAudioCtrl.hpp similarity index 96% rename from UI/adv-audio-control.hpp rename to frontend/components/OBSAdvAudioCtrl.hpp index 143b66222..6d12b3044 100644 --- a/UI/adv-audio-control.hpp +++ b/frontend/components/OBSAdvAudioCtrl.hpp @@ -1,17 +1,19 @@ #pragma once #include -#include -#include -#include -#include -#include "balance-slider.hpp" +#include +#include + +class BalanceSlider; +class QCheckBox; +class QComboBox; +class QDoubleSpinBox; class QGridLayout; class QLabel; class QSpinBox; -class QCheckBox; -class QComboBox; +class QStackedWidget; +class QWidget; enum class VolumeType { dB, diff --git a/UI/source-label.cpp b/frontend/components/OBSSourceLabel.cpp similarity index 95% rename from UI/source-label.cpp rename to frontend/components/OBSSourceLabel.cpp index 1abc1fa9a..efe81d958 100644 --- a/UI/source-label.cpp +++ b/frontend/components/OBSSourceLabel.cpp @@ -15,7 +15,8 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_source-label.cpp" +#include "OBSSourceLabel.hpp" +#include "moc_OBSSourceLabel.cpp" void OBSSourceLabel::SourceRenamed(void *data, calldata_t *params) { diff --git a/UI/source-label.hpp b/frontend/components/OBSSourceLabel.hpp similarity index 99% rename from UI/source-label.hpp rename to frontend/components/OBSSourceLabel.hpp index a73fa3c6d..c0e9f434f 100644 --- a/UI/source-label.hpp +++ b/frontend/components/OBSSourceLabel.hpp @@ -17,9 +17,10 @@ #pragma once -#include #include +#include + class OBSSourceLabel : public QLabel { Q_OBJECT; diff --git a/UI/scene-tree.cpp b/frontend/components/SceneTree.cpp similarity index 98% rename from UI/scene-tree.cpp rename to frontend/components/SceneTree.cpp index fc64b11f7..f96dce68c 100644 --- a/UI/scene-tree.cpp +++ b/frontend/components/SceneTree.cpp @@ -1,11 +1,9 @@ -#include "moc_scene-tree.cpp" +#include "SceneTree.hpp" -#include #include -#include -#include #include -#include + +#include "moc_SceneTree.cpp" SceneTree::SceneTree(QWidget *parent_) : QListWidget(parent_) { diff --git a/UI/scene-tree.hpp b/frontend/components/SceneTree.hpp similarity index 95% rename from UI/scene-tree.hpp rename to frontend/components/SceneTree.hpp index 5fc765a92..b4fb7dc67 100644 --- a/UI/scene-tree.hpp +++ b/frontend/components/SceneTree.hpp @@ -1,8 +1,9 @@ #pragma once #include -#include -#include +#include +#include +#include class SceneTree : public QListWidget { Q_OBJECT diff --git a/UI/ui-validation.cpp b/frontend/components/UIValidation.cpp similarity index 96% rename from UI/ui-validation.cpp rename to frontend/components/UIValidation.cpp index 482951604..3597949d0 100644 --- a/UI/ui-validation.cpp +++ b/frontend/components/UIValidation.cpp @@ -1,12 +1,11 @@ -#include "moc_ui-validation.cpp" +#include "UIValidation.hpp" + +#include -#include -#include #include #include -#include -#include +#include "moc_UIValidation.cpp" static int CountVideoSources() { diff --git a/UI/ui-validation.hpp b/frontend/components/UIValidation.hpp similarity index 97% rename from UI/ui-validation.hpp rename to frontend/components/UIValidation.hpp index 505db851d..dabccabd9 100644 --- a/UI/ui-validation.hpp +++ b/frontend/components/UIValidation.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include +#include + enum class StreamSettingsAction { OpenSettings, Cancel, diff --git a/UI/url-push-button.cpp b/frontend/components/UrlPushButton.cpp similarity index 83% rename from UI/url-push-button.cpp rename to frontend/components/UrlPushButton.cpp index 32c39620d..1e7b68d04 100644 --- a/UI/url-push-button.cpp +++ b/frontend/components/UrlPushButton.cpp @@ -1,9 +1,9 @@ -#include "moc_url-push-button.cpp" +#include "UrlPushButton.hpp" -#include -#include #include +#include "moc_UrlPushButton.cpp" + void UrlPushButton::setTargetUrl(QUrl url) { setToolTip(url.toString()); diff --git a/UI/url-push-button.hpp b/frontend/components/UrlPushButton.hpp similarity index 90% rename from UI/url-push-button.hpp rename to frontend/components/UrlPushButton.hpp index 2668ade27..bcf331738 100644 --- a/UI/url-push-button.hpp +++ b/frontend/components/UrlPushButton.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include class UrlPushButton : public QPushButton { Q_OBJECT From 819850c0f6450baada1cb9d9fb0dd2bb509eca77 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Thu, 21 Nov 2024 19:50:36 +0100 Subject: [PATCH 03/37] frontend: Prepare Qt UI components for splits --- .../ApplicationAudioCaptureToolbar.cpp | 0 .../ApplicationAudioCaptureToolbar.hpp | 0 frontend/components/AudioCaptureToolbar.cpp | 707 ++++++++ frontend/components/AudioCaptureToolbar.hpp | 178 ++ frontend/components/BrowserToolbar.cpp | 707 ++++++++ frontend/components/BrowserToolbar.hpp | 178 ++ frontend/components/ColorSourceToolbar.cpp | 707 ++++++++ frontend/components/ColorSourceToolbar.hpp | 178 ++ frontend/components/ComboSelectToolbar.cpp | 707 ++++++++ frontend/components/ComboSelectToolbar.hpp | 178 ++ frontend/components/DeviceCaptureToolbar.cpp | 707 ++++++++ frontend/components/DeviceCaptureToolbar.hpp | 178 ++ frontend/components/DisplayCaptureToolbar.cpp | 707 ++++++++ frontend/components/DisplayCaptureToolbar.hpp | 178 ++ frontend/components/GameCaptureToolbar.cpp | 707 ++++++++ frontend/components/GameCaptureToolbar.hpp | 178 ++ frontend/components/ImageSourceToolbar.cpp | 707 ++++++++ frontend/components/ImageSourceToolbar.hpp | 178 ++ .../components/OBSPreviewScalingComboBox.cpp | 0 .../components/OBSPreviewScalingComboBox.hpp | 0 .../components/OBSPreviewScalingLabel.cpp | 133 ++ .../components/OBSPreviewScalingLabel.hpp | 79 + frontend/components/SourceToolbar.cpp | 707 ++++++++ frontend/components/SourceToolbar.hpp | 178 ++ .../components/SourceTree.cpp | 0 .../components/SourceTree.hpp | 0 frontend/components/SourceTreeDelegate.cpp | 1613 +++++++++++++++++ frontend/components/SourceTreeDelegate.hpp | 202 +++ frontend/components/SourceTreeItem.cpp | 1613 +++++++++++++++++ frontend/components/SourceTreeItem.hpp | 202 +++ frontend/components/SourceTreeModel.cpp | 1613 +++++++++++++++++ frontend/components/SourceTreeModel.hpp | 202 +++ frontend/components/TextSourceToolbar.cpp | 707 ++++++++ frontend/components/TextSourceToolbar.hpp | 178 ++ .../components/VisibilityItemDelegate.cpp | 0 .../components/VisibilityItemDelegate.hpp | 0 frontend/components/VisibilityItemWidget.cpp | 133 ++ frontend/components/VisibilityItemWidget.hpp | 50 + frontend/components/WindowCaptureToolbar.cpp | 707 ++++++++ frontend/components/WindowCaptureToolbar.hpp | 178 ++ 40 files changed, 15575 insertions(+) rename UI/context-bar-controls.cpp => frontend/components/ApplicationAudioCaptureToolbar.cpp (100%) rename UI/context-bar-controls.hpp => frontend/components/ApplicationAudioCaptureToolbar.hpp (100%) create mode 100644 frontend/components/AudioCaptureToolbar.cpp create mode 100644 frontend/components/AudioCaptureToolbar.hpp create mode 100644 frontend/components/BrowserToolbar.cpp create mode 100644 frontend/components/BrowserToolbar.hpp create mode 100644 frontend/components/ColorSourceToolbar.cpp create mode 100644 frontend/components/ColorSourceToolbar.hpp create mode 100644 frontend/components/ComboSelectToolbar.cpp create mode 100644 frontend/components/ComboSelectToolbar.hpp create mode 100644 frontend/components/DeviceCaptureToolbar.cpp create mode 100644 frontend/components/DeviceCaptureToolbar.hpp create mode 100644 frontend/components/DisplayCaptureToolbar.cpp create mode 100644 frontend/components/DisplayCaptureToolbar.hpp create mode 100644 frontend/components/GameCaptureToolbar.cpp create mode 100644 frontend/components/GameCaptureToolbar.hpp create mode 100644 frontend/components/ImageSourceToolbar.cpp create mode 100644 frontend/components/ImageSourceToolbar.hpp rename UI/preview-controls.cpp => frontend/components/OBSPreviewScalingComboBox.cpp (100%) rename UI/preview-controls.hpp => frontend/components/OBSPreviewScalingComboBox.hpp (100%) create mode 100644 frontend/components/OBSPreviewScalingLabel.cpp create mode 100644 frontend/components/OBSPreviewScalingLabel.hpp create mode 100644 frontend/components/SourceToolbar.cpp create mode 100644 frontend/components/SourceToolbar.hpp rename UI/source-tree.cpp => frontend/components/SourceTree.cpp (100%) rename UI/source-tree.hpp => frontend/components/SourceTree.hpp (100%) create mode 100644 frontend/components/SourceTreeDelegate.cpp create mode 100644 frontend/components/SourceTreeDelegate.hpp create mode 100644 frontend/components/SourceTreeItem.cpp create mode 100644 frontend/components/SourceTreeItem.hpp create mode 100644 frontend/components/SourceTreeModel.cpp create mode 100644 frontend/components/SourceTreeModel.hpp create mode 100644 frontend/components/TextSourceToolbar.cpp create mode 100644 frontend/components/TextSourceToolbar.hpp rename UI/visibility-item-widget.cpp => frontend/components/VisibilityItemDelegate.cpp (100%) rename UI/visibility-item-widget.hpp => frontend/components/VisibilityItemDelegate.hpp (100%) create mode 100644 frontend/components/VisibilityItemWidget.cpp create mode 100644 frontend/components/VisibilityItemWidget.hpp create mode 100644 frontend/components/WindowCaptureToolbar.cpp create mode 100644 frontend/components/WindowCaptureToolbar.hpp diff --git a/UI/context-bar-controls.cpp b/frontend/components/ApplicationAudioCaptureToolbar.cpp similarity index 100% rename from UI/context-bar-controls.cpp rename to frontend/components/ApplicationAudioCaptureToolbar.cpp diff --git a/UI/context-bar-controls.hpp b/frontend/components/ApplicationAudioCaptureToolbar.hpp similarity index 100% rename from UI/context-bar-controls.hpp rename to frontend/components/ApplicationAudioCaptureToolbar.hpp diff --git a/frontend/components/AudioCaptureToolbar.cpp b/frontend/components/AudioCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/AudioCaptureToolbar.hpp b/frontend/components/AudioCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/BrowserToolbar.cpp b/frontend/components/BrowserToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/BrowserToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/BrowserToolbar.hpp b/frontend/components/BrowserToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/BrowserToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/ColorSourceToolbar.cpp b/frontend/components/ColorSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ColorSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/ColorSourceToolbar.hpp b/frontend/components/ColorSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ColorSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/ComboSelectToolbar.cpp b/frontend/components/ComboSelectToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ComboSelectToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/ComboSelectToolbar.hpp b/frontend/components/ComboSelectToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ComboSelectToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/DeviceCaptureToolbar.cpp b/frontend/components/DeviceCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/DeviceCaptureToolbar.hpp b/frontend/components/DeviceCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/DisplayCaptureToolbar.cpp b/frontend/components/DisplayCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/DisplayCaptureToolbar.hpp b/frontend/components/DisplayCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/GameCaptureToolbar.cpp b/frontend/components/GameCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/GameCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/GameCaptureToolbar.hpp b/frontend/components/GameCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/GameCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/frontend/components/ImageSourceToolbar.cpp b/frontend/components/ImageSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ImageSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/ImageSourceToolbar.hpp b/frontend/components/ImageSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ImageSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/UI/preview-controls.cpp b/frontend/components/OBSPreviewScalingComboBox.cpp similarity index 100% rename from UI/preview-controls.cpp rename to frontend/components/OBSPreviewScalingComboBox.cpp diff --git a/UI/preview-controls.hpp b/frontend/components/OBSPreviewScalingComboBox.hpp similarity index 100% rename from UI/preview-controls.hpp rename to frontend/components/OBSPreviewScalingComboBox.hpp diff --git a/frontend/components/OBSPreviewScalingLabel.cpp b/frontend/components/OBSPreviewScalingLabel.cpp new file mode 100644 index 000000000..7564e1bdb --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.cpp @@ -0,0 +1,133 @@ +/****************************************************************************** + Copyright (C) 2024 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 "preview-controls.hpp" +#include + +/* Preview Scale Label */ +void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) +{ + previewScale = scale; + UpdateScaleLabel(); +} + +void OBSPreviewScalingLabel::UpdateScaleLabel() +{ + float previewScalePercent = floor(100.0f * previewScale); + setText(QString::number(previewScalePercent) + "%"); +} + +/* Preview Scaling ComboBox */ +void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed) +{ + if (fixedScaling == fixed) + return; + + fixedScaling = fixed; + UpdateSelection(); +} + +void OBSPreviewScalingComboBox::CanvasResized(uint32_t width, uint32_t height) +{ + SetCanvasSize(width, height); + UpdateCanvasText(); +} + +void OBSPreviewScalingComboBox::OutputResized(uint32_t width, uint32_t height) +{ + SetOutputSize(width, height); + + bool canvasMatchesOutput = output_width == canvas_width && output_height == canvas_height; + + SetScaleOutputEnabled(!canvasMatchesOutput); + UpdateOutputText(); +} + +void OBSPreviewScalingComboBox::PreviewScaleChanged(float scale) +{ + previewScale = scale; + + if (fixedScaling) { + UpdateSelection(); + UpdateAllText(); + } else { + UpdateScaledText(); + } +} + +void OBSPreviewScalingComboBox::SetScaleOutputEnabled(bool show) +{ + if (scaleOutputEnabled == show) + return; + + scaleOutputEnabled = show; + + if (scaleOutputEnabled) { + addItem(QTStr("Basic.MainMenu.Edit.Scale.Output")); + } else { + removeItem(2); + } +} + +void OBSPreviewScalingComboBox::UpdateAllText() +{ + UpdateCanvasText(); + UpdateOutputText(); + UpdateScaledText(); +} + +void OBSPreviewScalingComboBox::UpdateCanvasText() +{ + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(canvas_width), QString::number(canvas_height)); + setItemText(1, text); +} + +void OBSPreviewScalingComboBox::UpdateOutputText() +{ + if (scaleOutputEnabled) { + QString text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(output_width), QString::number(output_height)); + setItemText(2, text); + } +} + +void OBSPreviewScalingComboBox::UpdateScaledText() +{ + QString text = QTStr("Basic.MainMenu.Edit.Scale.Manual"); + text = text.arg(QString::number(floor(canvas_width * previewScale)), + QString::number(floor(canvas_height * previewScale))); + setPlaceholderText(text); +} + +void OBSPreviewScalingComboBox::UpdateSelection() +{ + QSignalBlocker sb(this); + float outputScale = float(output_width) / float(canvas_width); + + if (!fixedScaling) { + setCurrentIndex(0); + } else { + if (previewScale == 1.0f) { + setCurrentIndex(1); + } else if (scaleOutputEnabled && (previewScale == outputScale)) { + setCurrentIndex(2); + } else { + setCurrentIndex(-1); + } + } +} diff --git a/frontend/components/OBSPreviewScalingLabel.hpp b/frontend/components/OBSPreviewScalingLabel.hpp new file mode 100644 index 000000000..4078dbea9 --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.hpp @@ -0,0 +1,79 @@ +/****************************************************************************** + Copyright (C) 2024 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 +#include + +class OBSPreviewScalingLabel : public QLabel { + Q_OBJECT + +public: + OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {} + +public slots: + void PreviewScaleChanged(float scale); + +private: + float previewScale = 0.0f; + void UpdateScaleLabel(); +}; + +class OBSPreviewScalingComboBox : public QComboBox { + Q_OBJECT + +public: + OBSPreviewScalingComboBox(QWidget *parent = nullptr) : QComboBox(parent) {} + + inline void SetCanvasSize(uint32_t width, uint32_t height) + { + canvas_width = width; + canvas_height = height; + }; + inline void SetOutputSize(uint32_t width, uint32_t height) + { + output_width = width; + output_height = height; + }; + void UpdateAllText(); + +public slots: + void PreviewScaleChanged(float scale); + void PreviewFixedScalingChanged(bool fixed); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + +private: + uint32_t canvas_width = 0; + uint32_t canvas_height = 0; + + uint32_t output_width = 0; + uint32_t output_height = 0; + + float previewScale = 0.0f; + + bool fixedScaling = false; + + bool scaleOutputEnabled = false; + void SetScaleOutputEnabled(bool show); + + void UpdateCanvasText(); + void UpdateOutputText(); + void UpdateScaledText(); + void UpdateSelection(); +}; diff --git a/frontend/components/SourceToolbar.cpp b/frontend/components/SourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/SourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/SourceToolbar.hpp b/frontend/components/SourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/SourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/UI/source-tree.cpp b/frontend/components/SourceTree.cpp similarity index 100% rename from UI/source-tree.cpp rename to frontend/components/SourceTree.cpp diff --git a/UI/source-tree.hpp b/frontend/components/SourceTree.hpp similarity index 100% rename from UI/source-tree.hpp rename to frontend/components/SourceTree.hpp diff --git a/frontend/components/SourceTreeDelegate.cpp b/frontend/components/SourceTreeDelegate.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeDelegate.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeDelegate.hpp b/frontend/components/SourceTreeDelegate.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeDelegate.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QLabel; +class OBSSourceLabel; +class QCheckBox; +class QLineEdit; +class SourceTree; +class QSpacerItem; +class QHBoxLayout; +class VisibilityItemWidget; + +class SourceTreeItem : public QFrame { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + bool IsEditing(); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + QLabel *iconLabel = nullptr; + QCheckBox *vis = nullptr; + QCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + OBSSourceLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + std::string newName; + + SourceTree *tree; + OBSSceneItem sceneitem; + std::vector sigs; + + virtual void paintEvent(QPaintEvent *event) override; + + void ExitEditModeInternal(bool save); + +private slots: + void Clear(); + + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void LockedChanged(bool locked); + + void ExpandClicked(bool checked); + + void Select(); + void Deselect(); +}; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; +}; + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + bool textPrepared = false; + QStaticText textNoSources; + QSvgRenderer iconNoSources; + + OBSData undoSceneData; + + bool iconsVisible = true; + + void UpdateNoSourcesMessage(); + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const { return ignoreReorder; } + inline void Clear() { GetStm()->Clear(); } + + inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } + inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } + inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + + void UpdateIcons(); + void SetIconsVisible(bool visible); + +public slots: + inline void ReorderItems() { GetStm()->ReorderItems(); } + inline void RefreshItems() { GetStm()->SceneChanged(); } + void Remove(OBSSceneItem item, OBSScene scene); + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + bool Edit(int idx); + void NewGroupEdit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + virtual void paintEvent(QPaintEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; + +class SourceTreeDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SourceTreeDelegate(QObject *parent); + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; diff --git a/frontend/components/SourceTreeItem.cpp b/frontend/components/SourceTreeItem.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeItem.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeItem.hpp b/frontend/components/SourceTreeItem.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeItem.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QLabel; +class OBSSourceLabel; +class QCheckBox; +class QLineEdit; +class SourceTree; +class QSpacerItem; +class QHBoxLayout; +class VisibilityItemWidget; + +class SourceTreeItem : public QFrame { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + bool IsEditing(); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + QLabel *iconLabel = nullptr; + QCheckBox *vis = nullptr; + QCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + OBSSourceLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + std::string newName; + + SourceTree *tree; + OBSSceneItem sceneitem; + std::vector sigs; + + virtual void paintEvent(QPaintEvent *event) override; + + void ExitEditModeInternal(bool save); + +private slots: + void Clear(); + + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void LockedChanged(bool locked); + + void ExpandClicked(bool checked); + + void Select(); + void Deselect(); +}; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; +}; + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + bool textPrepared = false; + QStaticText textNoSources; + QSvgRenderer iconNoSources; + + OBSData undoSceneData; + + bool iconsVisible = true; + + void UpdateNoSourcesMessage(); + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const { return ignoreReorder; } + inline void Clear() { GetStm()->Clear(); } + + inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } + inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } + inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + + void UpdateIcons(); + void SetIconsVisible(bool visible); + +public slots: + inline void ReorderItems() { GetStm()->ReorderItems(); } + inline void RefreshItems() { GetStm()->SceneChanged(); } + void Remove(OBSSceneItem item, OBSScene scene); + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + bool Edit(int idx); + void NewGroupEdit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + virtual void paintEvent(QPaintEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; + +class SourceTreeDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SourceTreeDelegate(QObject *parent); + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; diff --git a/frontend/components/SourceTreeModel.cpp b/frontend/components/SourceTreeModel.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeModel.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeModel.hpp b/frontend/components/SourceTreeModel.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeModel.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QLabel; +class OBSSourceLabel; +class QCheckBox; +class QLineEdit; +class SourceTree; +class QSpacerItem; +class QHBoxLayout; +class VisibilityItemWidget; + +class SourceTreeItem : public QFrame { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + bool IsEditing(); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + QLabel *iconLabel = nullptr; + QCheckBox *vis = nullptr; + QCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + OBSSourceLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + std::string newName; + + SourceTree *tree; + OBSSceneItem sceneitem; + std::vector sigs; + + virtual void paintEvent(QPaintEvent *event) override; + + void ExitEditModeInternal(bool save); + +private slots: + void Clear(); + + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void LockedChanged(bool locked); + + void ExpandClicked(bool checked); + + void Select(); + void Deselect(); +}; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; +}; + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + bool textPrepared = false; + QStaticText textNoSources; + QSvgRenderer iconNoSources; + + OBSData undoSceneData; + + bool iconsVisible = true; + + void UpdateNoSourcesMessage(); + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const { return ignoreReorder; } + inline void Clear() { GetStm()->Clear(); } + + inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } + inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } + inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + + void UpdateIcons(); + void SetIconsVisible(bool visible); + +public slots: + inline void ReorderItems() { GetStm()->ReorderItems(); } + inline void RefreshItems() { GetStm()->SceneChanged(); } + void Remove(OBSSceneItem item, OBSScene scene); + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + bool Edit(int idx); + void NewGroupEdit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + virtual void paintEvent(QPaintEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; + +class SourceTreeDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SourceTreeDelegate(QObject *parent); + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; diff --git a/frontend/components/TextSourceToolbar.cpp b/frontend/components/TextSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/TextSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/TextSourceToolbar.hpp b/frontend/components/TextSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/TextSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/UI/visibility-item-widget.cpp b/frontend/components/VisibilityItemDelegate.cpp similarity index 100% rename from UI/visibility-item-widget.cpp rename to frontend/components/VisibilityItemDelegate.cpp diff --git a/UI/visibility-item-widget.hpp b/frontend/components/VisibilityItemDelegate.hpp similarity index 100% rename from UI/visibility-item-widget.hpp rename to frontend/components/VisibilityItemDelegate.hpp diff --git a/frontend/components/VisibilityItemWidget.cpp b/frontend/components/VisibilityItemWidget.cpp new file mode 100644 index 000000000..50ea425ad --- /dev/null +++ b/frontend/components/VisibilityItemWidget.cpp @@ -0,0 +1,133 @@ +#include "moc_visibility-item-widget.cpp" +#include "obs-app.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_) + : source(source_), + enabledSignal(obs_source_get_signal_handler(source), "enable", OBSSourceEnabled, this) +{ + bool enabled = obs_source_enabled(source); + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + vis->setChecked(enabled); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + QHBoxLayout *itemLayout = new QHBoxLayout(); + itemLayout->addWidget(vis); + itemLayout->addWidget(label); + itemLayout->setContentsMargins(0, 0, 0, 0); + + setLayout(itemLayout); + + connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); }); +} + +void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data) +{ + VisibilityItemWidget *window = reinterpret_cast(param); + bool enabled = calldata_bool(data, "enabled"); + + QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled)); +} + +void VisibilityItemWidget::SourceEnabled(bool enabled) +{ + if (vis->isChecked() != enabled) + vis->setChecked(enabled); +} + +void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_) +{ + /* Do not update unless the state has actually changed */ + if (active_ == active && selected_ == selected) + return; + + QPalette pal = vis->palette(); + pal.setColor(QPalette::WindowText, color); + vis->setPalette(pal); + + label->setStyleSheet(QString("color: %1;").arg(color.name())); + + active = active_; + selected = selected_; +} + +VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyledItemDelegate::paint(painter, option, index); + + QObject *parentObj = parent(); + QListWidget *list = qobject_cast(parentObj); + if (!list) + return; + + QListWidgetItem *item = list->item(index.row()); + VisibilityItemWidget *widget = qobject_cast(list->itemWidget(item)); + if (!widget) + return; + + bool selected = option.state.testFlag(QStyle::State_Selected); + bool active = option.state.testFlag(QStyle::State_Active); + + QPalette palette = list->palette(); +#if defined(_WIN32) || defined(__APPLE__) + QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Inactive; +#else + QPalette::ColorGroup group = QPalette::Active; +#endif + +#ifdef _WIN32 + QPalette::ColorRole highlightRole = QPalette::WindowText; +#else + QPalette::ColorRole highlightRole = QPalette::HighlightedText; +#endif + + QPalette::ColorRole role; + + if (selected && active) + role = highlightRole; + else + role = QPalette::WindowText; + + widget->SetColor(palette.color(group, role), active, selected); +} + +bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event) +{ + QWidget *editor = qobject_cast(object); + if (!editor) + return false; + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) { + return false; + } + } + + return QStyledItemDelegate::eventFilter(object, event); +} + +void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source) +{ + VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source); + + list->setItemWidget(item, baseWidget); +} diff --git a/frontend/components/VisibilityItemWidget.hpp b/frontend/components/VisibilityItemWidget.hpp new file mode 100644 index 000000000..ec6b35e37 --- /dev/null +++ b/frontend/components/VisibilityItemWidget.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +class QLabel; +class QLineEdit; +class QListWidget; +class QListWidgetItem; +class QCheckBox; +class OBSSourceLabel; + +class VisibilityItemWidget : public QWidget { + Q_OBJECT + +private: + OBSSource source; + OBSSourceLabel *label = nullptr; + QCheckBox *vis = nullptr; + + OBSSignal enabledSignal; + + bool active = false; + bool selected = false; + + static void OBSSourceEnabled(void *param, calldata_t *data); + +private slots: + void SourceEnabled(bool enabled); + +public: + VisibilityItemWidget(obs_source_t *source); + + void SetColor(const QColor &color, bool active, bool selected); +}; + +class VisibilityItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + VisibilityItemDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; +}; + +void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source); diff --git a/frontend/components/WindowCaptureToolbar.cpp b/frontend/components/WindowCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#include "ui_browser-source-toolbar.h" +#include "ui_device-select-toolbar.h" +#include "ui_game-capture-toolbar.h" +#include "ui_image-source-toolbar.h" +#include "ui_color-source-toolbar.h" +#include "ui_text-source-toolbar.h" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +/* ========================================================================= */ + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} + +/* ========================================================================= */ + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} + +/* ========================================================================= */ + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} + +/* ========================================================================= */ + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} + +/* ========================================================================= */ + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} + +/* ========================================================================= */ + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +static inline long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +/* ========================================================================= */ + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/WindowCaptureToolbar.hpp b/frontend/components/WindowCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +class Ui_BrowserSourceToolbar; +class Ui_DeviceSelectToolbar; +class Ui_GameCaptureToolbar; +class Ui_ImageSourceToolbar; +class Ui_ColorSourceToolbar; +class Ui_TextSourceToolbar; + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; From f4fe30a5b38a95066eadc37598a2340159368943 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 18:22:25 +0100 Subject: [PATCH 04/37] frontend: Split Qt UI component into single file per C++ class --- .../ApplicationAudioCaptureToolbar.cpp | 689 +------ .../ApplicationAudioCaptureToolbar.hpp | 169 +- frontend/components/AudioCaptureToolbar.cpp | 678 +------ frontend/components/AudioCaptureToolbar.hpp | 169 +- frontend/components/BrowserToolbar.cpp | 688 +------ frontend/components/BrowserToolbar.hpp | 162 +- frontend/components/ColorSourceToolbar.cpp | 634 +------ frontend/components/ColorSourceToolbar.hpp | 159 +- frontend/components/ComboSelectToolbar.cpp | 611 +------ frontend/components/ComboSelectToolbar.hpp | 158 +- frontend/components/DeviceCaptureToolbar.cpp | 653 +------ frontend/components/DeviceCaptureToolbar.hpp | 155 +- frontend/components/DisplayCaptureToolbar.cpp | 671 +------ frontend/components/DisplayCaptureToolbar.hpp | 169 +- frontend/components/GameCaptureToolbar.cpp | 624 +------ frontend/components/GameCaptureToolbar.hpp | 159 +- frontend/components/ImageSourceToolbar.cpp | 660 +------ frontend/components/ImageSourceToolbar.hpp | 162 +- .../components/OBSPreviewScalingComboBox.cpp | 17 +- .../components/OBSPreviewScalingComboBox.hpp | 15 - .../components/OBSPreviewScalingLabel.cpp | 106 +- .../components/OBSPreviewScalingLabel.hpp | 45 - frontend/components/SourceToolbar.cpp | 653 +------ frontend/components/SourceToolbar.hpp | 151 +- frontend/components/SourceTree.cpp | 985 +--------- frontend/components/SourceTree.hpp | 133 +- frontend/components/SourceTreeDelegate.cpp | 1603 +---------------- frontend/components/SourceTreeDelegate.hpp | 195 +- frontend/components/SourceTreeItem.cpp | 1083 +---------- frontend/components/SourceTreeItem.hpp | 138 +- frontend/components/SourceTreeModel.cpp | 1187 +----------- frontend/components/SourceTreeModel.hpp | 161 +- frontend/components/TextSourceToolbar.cpp | 572 +----- frontend/components/TextSourceToolbar.hpp | 158 +- .../components/VisibilityItemDelegate.cpp | 73 +- .../components/VisibilityItemDelegate.hpp | 35 +- frontend/components/VisibilityItemWidget.cpp | 76 +- frontend/components/VisibilityItemWidget.hpp | 23 +- frontend/components/WindowCaptureToolbar.cpp | 670 +------ frontend/components/WindowCaptureToolbar.hpp | 169 +- 40 files changed, 108 insertions(+), 15510 deletions(-) diff --git a/frontend/components/ApplicationAudioCaptureToolbar.cpp b/frontend/components/ApplicationAudioCaptureToolbar.cpp index c45d06976..b91a00b11 100644 --- a/frontend/components/ApplicationAudioCaptureToolbar.cpp +++ b/frontend/components/ApplicationAudioCaptureToolbar.cpp @@ -1,255 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "ApplicationAudioCaptureToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} +#include "moc_ApplicationAudioCaptureToolbar.cpp" ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) @@ -269,439 +20,3 @@ void ApplicationAudioCaptureToolbar::Init() ComboSelectToolbar::Init(); } - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/ApplicationAudioCaptureToolbar.hpp b/frontend/components/ApplicationAudioCaptureToolbar.hpp index acf88a5fb..bb916e762 100644 --- a/frontend/components/ApplicationAudioCaptureToolbar.hpp +++ b/frontend/components/ApplicationAudioCaptureToolbar.hpp @@ -1,85 +1,6 @@ #pragma once -#include -#include -#include - -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; +#include "ComboSelectToolbar.hpp" class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { Q_OBJECT @@ -88,91 +9,3 @@ public: ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); void Init() override; }; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/AudioCaptureToolbar.cpp b/frontend/components/AudioCaptureToolbar.cpp index c45d06976..f61eb6eae 100644 --- a/frontend/components/AudioCaptureToolbar.cpp +++ b/frontend/components/AudioCaptureToolbar.cpp @@ -1,18 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "AudioCaptureToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" +#include "moc_AudioCaptureToolbar.cpp" #ifdef _WIN32 #define get_os_module(win, mac, linux) obs_get_module(win) @@ -25,186 +13,6 @@ #define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) #endif -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} void AudioCaptureToolbar::Init() @@ -223,485 +31,3 @@ void AudioCaptureToolbar::Init() ComboSelectToolbar::Init(); } - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/AudioCaptureToolbar.hpp b/frontend/components/AudioCaptureToolbar.hpp index acf88a5fb..713ea322f 100644 --- a/frontend/components/AudioCaptureToolbar.hpp +++ b/frontend/components/AudioCaptureToolbar.hpp @@ -1,69 +1,6 @@ #pragma once -#include -#include -#include - -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; +#include "ComboSelectToolbar.hpp" class AudioCaptureToolbar : public ComboSelectToolbar { Q_OBJECT @@ -72,107 +9,3 @@ public: AudioCaptureToolbar(QWidget *parent, OBSSource source); void Init() override; }; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/BrowserToolbar.cpp b/frontend/components/BrowserToolbar.cpp index c45d06976..b41360dd5 100644 --- a/frontend/components/BrowserToolbar.cpp +++ b/frontend/components/BrowserToolbar.cpp @@ -1,88 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - +#include "BrowserToolbar.hpp" #include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ +#include "moc_BrowserToolbar.cpp" BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) : SourceToolbar(parent, source), @@ -103,605 +21,3 @@ void BrowserToolbar::on_refresh_clicked() obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); obs_property_button_clicked(p, source.Get()); } - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/BrowserToolbar.hpp b/frontend/components/BrowserToolbar.hpp index acf88a5fb..6f37223c3 100644 --- a/frontend/components/BrowserToolbar.hpp +++ b/frontend/components/BrowserToolbar.hpp @@ -1,39 +1,8 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; class BrowserToolbar : public SourceToolbar { Q_OBJECT @@ -47,132 +16,3 @@ public: public slots: void on_refresh_clicked(); }; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/ColorSourceToolbar.cpp b/frontend/components/ColorSourceToolbar.cpp index c45d06976..dc4081acf 100644 --- a/frontend/components/ColorSourceToolbar.cpp +++ b/frontend/components/ColorSourceToolbar.cpp @@ -1,500 +1,16 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" +#include "ColorSourceToolbar.hpp" #include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif +#include -/* ========================================================================= */ +#include "moc_ColorSourceToolbar.cpp" -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) +QColor color_from_int(long long val) { return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); } -static inline long long color_to_int(QColor color) +long long color_to_int(QColor color) { auto shift = [&](unsigned val, int shift) { return ((val & 0xff) << shift); @@ -565,143 +81,3 @@ void ColorSourceToolbar::on_choose_clicked() SetUndoProperties(source); } - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/ColorSourceToolbar.hpp b/frontend/components/ColorSourceToolbar.hpp index acf88a5fb..dc05f0736 100644 --- a/frontend/components/ColorSourceToolbar.hpp +++ b/frontend/components/ColorSourceToolbar.hpp @@ -1,148 +1,8 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; class ColorSourceToolbar : public SourceToolbar { Q_OBJECT @@ -159,20 +19,3 @@ public: public slots: void on_choose_clicked(); }; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/ComboSelectToolbar.cpp b/frontend/components/ComboSelectToolbar.cpp index c45d06976..77a0364d7 100644 --- a/frontend/components/ComboSelectToolbar.cpp +++ b/frontend/components/ComboSelectToolbar.cpp @@ -1,110 +1,10 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "ComboSelectToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif +#include +#include -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ +#include "moc_ComboSelectToolbar.cpp" ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) : SourceToolbar(parent, source), @@ -115,7 +15,7 @@ ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) ComboSelectToolbar::~ComboSelectToolbar() {} -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) { size_t count = obs_property_list_item_count(p); int cur_idx = -1; @@ -204,504 +104,3 @@ void ComboSelectToolbar::on_device_currentIndexChanged(int idx) UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); SetUndoProperties(source); } - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/ComboSelectToolbar.hpp b/frontend/components/ComboSelectToolbar.hpp index acf88a5fb..613709e13 100644 --- a/frontend/components/ComboSelectToolbar.hpp +++ b/frontend/components/ComboSelectToolbar.hpp @@ -1,52 +1,8 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" -class Ui_BrowserSourceToolbar; class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; class ComboSelectToolbar : public SourceToolbar { Q_OBJECT @@ -64,115 +20,3 @@ public: public slots: void on_device_currentIndexChanged(int idx); }; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/DeviceCaptureToolbar.cpp b/frontend/components/DeviceCaptureToolbar.cpp index c45d06976..c69179453 100644 --- a/frontend/components/DeviceCaptureToolbar.cpp +++ b/frontend/components/DeviceCaptureToolbar.cpp @@ -1,302 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "DeviceCaptureToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ +#include "moc_DeviceCaptureToolbar.cpp" DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) : QWidget(parent), @@ -352,356 +56,3 @@ void DeviceCaptureToolbar::on_activateButton_clicked() proc_handler_call(ph, "activate", &cd); calldata_free(&cd); } - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/DeviceCaptureToolbar.hpp b/frontend/components/DeviceCaptureToolbar.hpp index acf88a5fb..17f76f4aa 100644 --- a/frontend/components/DeviceCaptureToolbar.hpp +++ b/frontend/components/DeviceCaptureToolbar.hpp @@ -1,101 +1,10 @@ #pragma once -#include #include + #include -class Ui_BrowserSourceToolbar; class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; class DeviceCaptureToolbar : public QWidget { Q_OBJECT @@ -114,65 +23,3 @@ public: public slots: void on_activateButton_clicked(); }; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/DisplayCaptureToolbar.cpp b/frontend/components/DisplayCaptureToolbar.cpp index c45d06976..d32bdea84 100644 --- a/frontend/components/DisplayCaptureToolbar.cpp +++ b/frontend/components/DisplayCaptureToolbar.cpp @@ -1,18 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "DisplayCaptureToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" +#include "moc_DisplayCaptureToolbar.cpp" #ifdef _WIN32 #define get_os_module(win, mac, linux) obs_get_module(win) @@ -25,251 +13,6 @@ #define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) #endif -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} void DisplayCaptureToolbar::Init() @@ -295,413 +38,3 @@ void DisplayCaptureToolbar::Init() ComboSelectToolbar::Init(); } - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/DisplayCaptureToolbar.hpp b/frontend/components/DisplayCaptureToolbar.hpp index acf88a5fb..0f2f3bb13 100644 --- a/frontend/components/DisplayCaptureToolbar.hpp +++ b/frontend/components/DisplayCaptureToolbar.hpp @@ -1,93 +1,6 @@ #pragma once -#include -#include -#include - -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; +#include "ComboSelectToolbar.hpp" class DisplayCaptureToolbar : public ComboSelectToolbar { Q_OBJECT @@ -96,83 +9,3 @@ public: DisplayCaptureToolbar(QWidget *parent, OBSSource source); void Init() override; }; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/GameCaptureToolbar.cpp b/frontend/components/GameCaptureToolbar.cpp index c45d06976..2a9f8afdf 100644 --- a/frontend/components/GameCaptureToolbar.cpp +++ b/frontend/components/GameCaptureToolbar.cpp @@ -1,359 +1,11 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" +#include "GameCaptureToolbar.hpp" +#include "ui_game-capture-toolbar.h" #include -#include -#include -#include -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" +#include "moc_GameCaptureToolbar.cpp" -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ +extern int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false); GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) : SourceToolbar(parent, source), @@ -437,271 +89,3 @@ void GameCaptureToolbar::on_window_currentIndexChanged(int idx) obs_source_update(source, settings); SetUndoProperties(source); } - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/GameCaptureToolbar.hpp b/frontend/components/GameCaptureToolbar.hpp index acf88a5fb..0dcf769e3 100644 --- a/frontend/components/GameCaptureToolbar.hpp +++ b/frontend/components/GameCaptureToolbar.hpp @@ -1,119 +1,8 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; class GameCaptureToolbar : public SourceToolbar { Q_OBJECT @@ -130,49 +19,3 @@ public slots: void on_mode_currentIndexChanged(int idx); void on_window_currentIndexChanged(int idx); }; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/ImageSourceToolbar.cpp b/frontend/components/ImageSourceToolbar.cpp index c45d06976..71458cef2 100644 --- a/frontend/components/ImageSourceToolbar.cpp +++ b/frontend/components/ImageSourceToolbar.cpp @@ -1,444 +1,9 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" +#include "ImageSourceToolbar.hpp" +#include "ui_image-source-toolbar.h" #include -#include -#include -#include -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ +#include "moc_ImageSourceToolbar.cpp" ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) : SourceToolbar(parent, source), @@ -486,222 +51,3 @@ void ImageSourceToolbar::on_browse_clicked() obs_source_update(source, settings); SetUndoProperties(source); } - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/ImageSourceToolbar.hpp b/frontend/components/ImageSourceToolbar.hpp index acf88a5fb..700b90b38 100644 --- a/frontend/components/ImageSourceToolbar.hpp +++ b/frontend/components/ImageSourceToolbar.hpp @@ -1,135 +1,8 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; class ImageSourceToolbar : public SourceToolbar { Q_OBJECT @@ -143,36 +16,3 @@ public: public slots: void on_browse_clicked(); }; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/OBSPreviewScalingComboBox.cpp b/frontend/components/OBSPreviewScalingComboBox.cpp index 7564e1bdb..c1a08fcb6 100644 --- a/frontend/components/OBSPreviewScalingComboBox.cpp +++ b/frontend/components/OBSPreviewScalingComboBox.cpp @@ -15,23 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "preview-controls.hpp" -#include +#include "OBSPreviewScalingComboBox.hpp" -/* Preview Scale Label */ -void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) -{ - previewScale = scale; - UpdateScaleLabel(); -} +#include -void OBSPreviewScalingLabel::UpdateScaleLabel() -{ - float previewScalePercent = floor(100.0f * previewScale); - setText(QString::number(previewScalePercent) + "%"); -} +#include "moc_OBSPreviewScalingComboBox.cpp" -/* Preview Scaling ComboBox */ void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed) { if (fixedScaling == fixed) diff --git a/frontend/components/OBSPreviewScalingComboBox.hpp b/frontend/components/OBSPreviewScalingComboBox.hpp index 4078dbea9..73c424369 100644 --- a/frontend/components/OBSPreviewScalingComboBox.hpp +++ b/frontend/components/OBSPreviewScalingComboBox.hpp @@ -17,23 +17,8 @@ #pragma once -#include #include -class OBSPreviewScalingLabel : public QLabel { - Q_OBJECT - -public: - OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {} - -public slots: - void PreviewScaleChanged(float scale); - -private: - float previewScale = 0.0f; - void UpdateScaleLabel(); -}; - class OBSPreviewScalingComboBox : public QComboBox { Q_OBJECT diff --git a/frontend/components/OBSPreviewScalingLabel.cpp b/frontend/components/OBSPreviewScalingLabel.cpp index 7564e1bdb..580b05836 100644 --- a/frontend/components/OBSPreviewScalingLabel.cpp +++ b/frontend/components/OBSPreviewScalingLabel.cpp @@ -15,10 +15,9 @@ along with this program. If not, see . ******************************************************************************/ -#include "preview-controls.hpp" -#include +#include "OBSPreviewScalingLabel.hpp" +#include "moc_OBSPreviewScalingLabel.cpp" -/* Preview Scale Label */ void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) { previewScale = scale; @@ -30,104 +29,3 @@ void OBSPreviewScalingLabel::UpdateScaleLabel() float previewScalePercent = floor(100.0f * previewScale); setText(QString::number(previewScalePercent) + "%"); } - -/* Preview Scaling ComboBox */ -void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed) -{ - if (fixedScaling == fixed) - return; - - fixedScaling = fixed; - UpdateSelection(); -} - -void OBSPreviewScalingComboBox::CanvasResized(uint32_t width, uint32_t height) -{ - SetCanvasSize(width, height); - UpdateCanvasText(); -} - -void OBSPreviewScalingComboBox::OutputResized(uint32_t width, uint32_t height) -{ - SetOutputSize(width, height); - - bool canvasMatchesOutput = output_width == canvas_width && output_height == canvas_height; - - SetScaleOutputEnabled(!canvasMatchesOutput); - UpdateOutputText(); -} - -void OBSPreviewScalingComboBox::PreviewScaleChanged(float scale) -{ - previewScale = scale; - - if (fixedScaling) { - UpdateSelection(); - UpdateAllText(); - } else { - UpdateScaledText(); - } -} - -void OBSPreviewScalingComboBox::SetScaleOutputEnabled(bool show) -{ - if (scaleOutputEnabled == show) - return; - - scaleOutputEnabled = show; - - if (scaleOutputEnabled) { - addItem(QTStr("Basic.MainMenu.Edit.Scale.Output")); - } else { - removeItem(2); - } -} - -void OBSPreviewScalingComboBox::UpdateAllText() -{ - UpdateCanvasText(); - UpdateOutputText(); - UpdateScaledText(); -} - -void OBSPreviewScalingComboBox::UpdateCanvasText() -{ - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(canvas_width), QString::number(canvas_height)); - setItemText(1, text); -} - -void OBSPreviewScalingComboBox::UpdateOutputText() -{ - if (scaleOutputEnabled) { - QString text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(output_width), QString::number(output_height)); - setItemText(2, text); - } -} - -void OBSPreviewScalingComboBox::UpdateScaledText() -{ - QString text = QTStr("Basic.MainMenu.Edit.Scale.Manual"); - text = text.arg(QString::number(floor(canvas_width * previewScale)), - QString::number(floor(canvas_height * previewScale))); - setPlaceholderText(text); -} - -void OBSPreviewScalingComboBox::UpdateSelection() -{ - QSignalBlocker sb(this); - float outputScale = float(output_width) / float(canvas_width); - - if (!fixedScaling) { - setCurrentIndex(0); - } else { - if (previewScale == 1.0f) { - setCurrentIndex(1); - } else if (scaleOutputEnabled && (previewScale == outputScale)) { - setCurrentIndex(2); - } else { - setCurrentIndex(-1); - } - } -} diff --git a/frontend/components/OBSPreviewScalingLabel.hpp b/frontend/components/OBSPreviewScalingLabel.hpp index 4078dbea9..82dbf39b8 100644 --- a/frontend/components/OBSPreviewScalingLabel.hpp +++ b/frontend/components/OBSPreviewScalingLabel.hpp @@ -18,7 +18,6 @@ #pragma once #include -#include class OBSPreviewScalingLabel : public QLabel { Q_OBJECT @@ -33,47 +32,3 @@ private: float previewScale = 0.0f; void UpdateScaleLabel(); }; - -class OBSPreviewScalingComboBox : public QComboBox { - Q_OBJECT - -public: - OBSPreviewScalingComboBox(QWidget *parent = nullptr) : QComboBox(parent) {} - - inline void SetCanvasSize(uint32_t width, uint32_t height) - { - canvas_width = width; - canvas_height = height; - }; - inline void SetOutputSize(uint32_t width, uint32_t height) - { - output_width = width; - output_height = height; - }; - void UpdateAllText(); - -public slots: - void PreviewScaleChanged(float scale); - void PreviewFixedScalingChanged(bool fixed); - void CanvasResized(uint32_t width, uint32_t height); - void OutputResized(uint32_t width, uint32_t height); - -private: - uint32_t canvas_width = 0; - uint32_t canvas_height = 0; - - uint32_t output_width = 0; - uint32_t output_height = 0; - - float previewScale = 0.0f; - - bool fixedScaling = false; - - bool scaleOutputEnabled = false; - void SetScaleOutputEnabled(bool show); - - void UpdateCanvasText(); - void UpdateOutputText(); - void UpdateScaledText(); - void UpdateSelection(); -}; diff --git a/frontend/components/SourceToolbar.cpp b/frontend/components/SourceToolbar.cpp index c45d06976..da8675aa6 100644 --- a/frontend/components/SourceToolbar.cpp +++ b/frontend/components/SourceToolbar.cpp @@ -1,31 +1,8 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" +#include "SourceToolbar.hpp" -#include -#include -#include -#include +#include -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ +#include "moc_SourceToolbar.cpp" SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) : QWidget(parent), @@ -81,627 +58,3 @@ void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) oldData = nullptr; } - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/SourceToolbar.hpp b/frontend/components/SourceToolbar.hpp index acf88a5fb..8e3f9b5f7 100644 --- a/frontend/components/SourceToolbar.hpp +++ b/frontend/components/SourceToolbar.hpp @@ -1,15 +1,8 @@ #pragma once -#include #include -#include -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; +#include class SourceToolbar : public QWidget { Q_OBJECT @@ -34,145 +27,3 @@ public: public slots: virtual void Update() {} }; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/frontend/components/SourceTree.cpp b/frontend/components/SourceTree.cpp index a0242e3cd..0325b59d8 100644 --- a/frontend/components/SourceTree.cpp +++ b/frontend/components/SourceTree.cpp @@ -1,26 +1,11 @@ -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "source-tree.hpp" -#include "platform.hpp" -#include "source-label.hpp" +#include "SourceTree.hpp" +#include "SourceTreeDelegate.hpp" -#include -#include -#include +#include -#include +#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include +#include "moc_SourceTree.cpp" static inline OBSScene GetCurrentScene() { @@ -28,610 +13,6 @@ static inline OBSScene GetCurrentScene() return main->GetCurrentScene(); } -/* ========================================================================= */ - -SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) -{ - setAttribute(Qt::WA_TranslucentBackground); - setMouseTracking(true); - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - const char *name = obs_source_get_name(source); - - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); - int preset = obs_data_get_int(privData, "color-preset"); - - if (preset == 1) { - const char *color = obs_data_get_string(privData, "color"); - std::string col = "background: "; - col += color; - setStyleSheet(col.c_str()); - } else if (preset > 1) { - setStyleSheet(""); - setProperty("bgColor", preset - 1); - } else { - setStyleSheet("background: none"); - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *id = obs_source_get_id(source); - - bool sourceVisible = obs_sceneitem_visible(sceneitem); - - if (tree->iconsVisible) { - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = main->GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = main->GetGroupIcon(); - else - icon = main->GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - - iconLabel = new QLabel(); - iconLabel->setPixmap(pixmap); - iconLabel->setEnabled(sourceVisible); - iconLabel->setStyleSheet("background: none"); - iconLabel->setProperty("class", "source-icon"); - } - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setChecked(sourceVisible); - vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); - vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); - - lock = new QCheckBox(); - lock->setProperty("class", "checkbox-icon indicator-lock"); - lock->setChecked(obs_sceneitem_locked(sceneitem)); - lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); - lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - label->setAttribute(Qt::WA_TranslucentBackground); - label->setEnabled(sourceVisible); - -#ifdef __APPLE__ - vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); - lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - boxLayout = new QHBoxLayout(); - - boxLayout->setContentsMargins(0, 0, 0, 0); - boxLayout->setSpacing(0); - if (iconLabel) { - boxLayout->addWidget(iconLabel); - boxLayout->addSpacing(2); - } - boxLayout->addWidget(label); - boxLayout->addWidget(vis); - boxLayout->addWidget(lock); -#ifdef __APPLE__ - /* Hack: Fixes a bug where scrollbars would be above the lock icon */ - boxLayout->addSpacing(16); -#endif - - Update(false); - - setLayout(boxLayout); - - /* --------------------------------------------------------- */ - - auto setItemVisible = [this](bool val) { - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *scenesource = obs_scene_get_source(scene); - int64_t id = obs_sceneitem_get_id(sceneitem); - const char *name = obs_source_get_name(scenesource); - const char *uuid = obs_source_get_uuid(scenesource); - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - - auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { - OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); - obs_scene_t *sc = obs_group_or_scene_from_source(s); - obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); - if (si) - obs_sceneitem_set_visible(si, val); - }; - - QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); - - OBSBasic *main = OBSBasic::Get(); - main->undo_s.add_action(str.arg(obs_source_get_name(source), name), - std::bind(undo_redo, std::placeholders::_1, id, !val), - std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); - - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_visible(sceneitem, val); - }; - - auto setItemLocked = [this](bool checked) { - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_locked(sceneitem, checked); - }; - - connect(vis, &QAbstractButton::clicked, setItemVisible); - connect(lock, &QAbstractButton::clicked, setItemLocked); -} - -void SourceTreeItem::paintEvent(QPaintEvent *event) -{ - QStyleOption opt; - opt.initFrom(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - QWidget::paintEvent(event); -} - -void SourceTreeItem::DisconnectSignals() -{ - sigs.clear(); -} - -void SourceTreeItem::Clear() -{ - DisconnectSignals(); - sceneitem = nullptr; -} - -void SourceTreeItem::ReconnectSignals() -{ - if (!sceneitem) - return; - - DisconnectSignals(); - - /* --------------------------------------------------------- */ - - auto removeItem = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); - - if (curItem == this_->sceneitem) { - QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), - Q_ARG(OBSScene, curScene)); - curItem = nullptr; - } - if (!curItem) - QMetaObject::invokeMethod(this_, "Clear"); - }; - - auto itemVisible = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool visible = calldata_bool(cd, "visible"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); - }; - - auto itemLocked = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool locked = calldata_bool(cd, "locked"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); - }; - - auto itemSelect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Select"); - }; - - auto itemDeselect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Deselect"); - }; - - auto reorderGroup = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - QMetaObject::invokeMethod(this_->tree, "ReorderItems"); - }; - - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *sceneSource = obs_scene_get_source(scene); - signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); - - sigs.emplace_back(signal, "remove", removeItem, this); - sigs.emplace_back(signal, "item_remove", removeItem, this); - sigs.emplace_back(signal, "item_visible", itemVisible, this); - sigs.emplace_back(signal, "item_locked", itemLocked, this); - sigs.emplace_back(signal, "item_select", itemSelect, this); - sigs.emplace_back(signal, "item_deselect", itemDeselect, this); - - if (obs_sceneitem_is_group(sceneitem)) { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - - sigs.emplace_back(signal, "reorder", reorderGroup, this); - } - - /* --------------------------------------------------------- */ - - auto removeSource = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - this_->DisconnectSignals(); - this_->sceneitem = nullptr; - QMetaObject::invokeMethod(this_->tree, "RefreshItems"); - }; - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - sigs.emplace_back(signal, "remove", removeSource, this); -} - -void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); - - if (expand) { - expand->setChecked(!expand->isChecked()); - } else { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - if (obs_source_configurable(source)) { - main->CreatePropertiesWindow(source); - } - } -} - -void SourceTreeItem::enterEvent(QEnterEvent *event) -{ - QWidget::enterEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); - preview->hoveredPreviewItems.push_back(sceneitem); -} - -void SourceTreeItem::leaveEvent(QEvent *event) -{ - QWidget::leaveEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); -} - -bool SourceTreeItem::IsEditing() -{ - return editor != nullptr; -} - -void SourceTreeItem::EnterEditMode() -{ - setFocusPolicy(Qt::StrongFocus); - int index = boxLayout->indexOf(label); - boxLayout->removeWidget(label); - editor = new QLineEdit(label->text()); - editor->setStyleSheet("background: none"); - editor->selectAll(); - editor->installEventFilter(this); - boxLayout->insertWidget(index, editor); - setFocusProxy(editor); -} - -void SourceTreeItem::ExitEditMode(bool save) -{ - ExitEditModeInternal(save); - - if (tree->undoSceneData) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); - main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); - - tree->undoSceneData = nullptr; - } -} - -void SourceTreeItem::ExitEditModeInternal(bool save) -{ - if (!editor) { - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSScene scene = main->GetCurrentScene(); - - newName = QT_TO_UTF8(editor->text()); - - setFocusProxy(nullptr); - int index = boxLayout->indexOf(editor); - boxLayout->removeWidget(editor); - delete editor; - editor = nullptr; - setFocusPolicy(Qt::NoFocus); - boxLayout->insertWidget(index, label); - setFocus(); - - /* ----------------------------------------- */ - /* check for empty string */ - - if (!save) - return; - - if (newName.empty()) { - OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; - } - - /* ----------------------------------------- */ - /* Check for same name */ - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - if (newName == obs_source_get_name(source)) - return; - - /* ----------------------------------------- */ - /* check for existing source */ - - OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); - bool exists = !!existingSource; - - if (exists) { - OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - return; - } - - /* ----------------------------------------- */ - /* rename */ - - QSignalBlocker sourcesSignalBlocker(this); - std::string prevName(obs_source_get_name(source)); - std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); - auto undo = [scene_uuid, prevName, main](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prevName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - std::string editedName = newName; - - auto redo = [scene_uuid, main, editedName](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, editedName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - const char *uuid = obs_source_get_uuid(source); - main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); - - obs_source_set_name(source, newName.c_str()); -} - -bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) -{ - if (editor != object) - return false; - - if (LineEditCanceled(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); - return true; - } - if (LineEditChanged(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); - return true; - } - - return false; -} - -void SourceTreeItem::VisibilityChanged(bool visible) -{ - if (iconLabel) { - iconLabel->setEnabled(visible); - } - label->setEnabled(visible); - vis->setChecked(visible); -} - -void SourceTreeItem::LockedChanged(bool locked) -{ - lock->setChecked(locked); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Update(bool force) -{ - OBSScene scene = GetCurrentScene(); - obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); - - Type newType; - - /* ------------------------------------------------- */ - /* if it's a group item, insert group checkbox */ - - if (obs_sceneitem_is_group(sceneitem)) { - newType = Type::Group; - - /* ------------------------------------------------- */ - /* if it's a group sub-item */ - - } else if (itemScene != scene) { - newType = Type::SubItem; - - /* ------------------------------------------------- */ - /* if it's a regular item */ - - } else { - newType = Type::Item; - } - - /* ------------------------------------------------- */ - - if (!force && newType == type) { - return; - } - - /* ------------------------------------------------- */ - - ReconnectSignals(); - - if (spacer) { - boxLayout->removeItem(spacer); - delete spacer; - spacer = nullptr; - } - - if (type == Type::Group) { - boxLayout->removeWidget(expand); - expand->deleteLater(); - expand = nullptr; - } - - type = newType; - - if (type == Type::SubItem) { - spacer = new QSpacerItem(16, 1); - boxLayout->insertItem(0, spacer); - - } else if (type == Type::Group) { - expand = new QCheckBox(); - expand->setProperty("class", "checkbox-icon indicator-expand"); -#ifdef __APPLE__ - expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - boxLayout->insertWidget(0, expand); - - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - expand->blockSignals(true); - expand->setChecked(obs_data_get_bool(data, "collapsed")); - expand->blockSignals(false); - - connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); - - } else { - spacer = new QSpacerItem(3, 1); - boxLayout->insertItem(0, spacer); - } -} - -void SourceTreeItem::ExpandClicked(bool checked) -{ - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - - obs_data_set_bool(data, "collapsed", checked); - - if (!checked) - tree->GetStm()->ExpandGroup(sceneitem); - else - tree->GetStm()->CollapseGroup(sceneitem); -} - -void SourceTreeItem::Select() -{ - tree->SelectItem(sceneitem, true); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Deselect() -{ - tree->SelectItem(sceneitem, false); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -/* ========================================================================= */ - -void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) -{ - SourceTreeModel *stm = reinterpret_cast(ptr); - - switch (event) { - case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: - stm->SceneChanged(); - break; - case OBS_FRONTEND_EVENT_EXIT: - stm->Clear(); - obs_frontend_remove_event_callback(OBSFrontendEvent, stm); - break; - case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: - stm->Clear(); - break; - default: - break; - } -} - -void SourceTreeModel::Clear() -{ - beginResetModel(); - items.clear(); - endResetModel(); - - hasGroups = false; -} - -static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) -{ - QVector &items = *reinterpret_cast *>(ptr); - - obs_source_t *src = obs_sceneitem_get_source(item); - if (obs_source_removed(src)) { - return true; - } - - if (obs_sceneitem_is_group(item)) { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); - - bool collapse = obs_data_get_bool(data, "collapsed"); - if (!collapse) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - obs_scene_enum_items(scene, enumItem, &items); - } - } - - items.insert(0, item); - return true; -} - -void SourceTreeModel::SceneChanged() -{ - OBSScene scene = GetCurrentScene(); - - beginResetModel(); - items.clear(); - obs_scene_enum_items(scene, enumItem, &items); - endResetModel(); - - UpdateGroupState(false); - st->ResetWidgets(); - - for (int i = 0; i < items.count(); i++) { - bool select = obs_sceneitem_selected(items[i]); - QModelIndex index = createIndex(i, 0); - - st->selectionModel()->select(index, - select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); - } -} - /* moves a scene item index (blame linux distros for using older Qt builds) */ static inline void MoveItem(QVector &items, int oldIdx, int newIdx) { @@ -640,347 +21,6 @@ static inline void MoveItem(QVector &items, int oldIdx, int newIdx items.insert(newIdx, item); } -/* reorders list optimally with model reorder funcs */ -void SourceTreeModel::ReorderItems() -{ - OBSScene scene = GetCurrentScene(); - - QVector newitems; - obs_scene_enum_items(scene, enumItem, &newitems); - - /* if item list has changed size, do full reset */ - if (newitems.count() != items.count()) { - SceneChanged(); - return; - } - - for (;;) { - int idx1Old = 0; - int idx1New = 0; - int count; - int i; - - /* find first starting changed item index */ - for (i = 0; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[i]; - obs_sceneitem_t *newItem = newitems[i]; - if (oldItem != newItem) { - idx1Old = i; - break; - } - } - - /* if everything is the same, break */ - if (i == newitems.count()) { - break; - } - - /* find new starting index */ - for (i = idx1Old + 1; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[idx1Old]; - obs_sceneitem_t *newItem = newitems[i]; - - if (oldItem == newItem) { - idx1New = i; - break; - } - } - - /* if item could not be found, do full reset */ - if (i == newitems.count()) { - SceneChanged(); - return; - } - - /* get move count */ - for (count = 1; (idx1New + count) < newitems.count(); count++) { - int oldIdx = idx1Old + count; - int newIdx = idx1New + count; - - obs_sceneitem_t *oldItem = items[oldIdx]; - obs_sceneitem_t *newItem = newitems[newIdx]; - - if (oldItem != newItem) { - break; - } - } - - /* move items */ - beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); - for (i = 0; i < count; i++) { - int to = idx1New + count; - if (to > idx1Old) - to--; - MoveItem(items, idx1Old, to); - } - endMoveRows(); - } -} - -void SourceTreeModel::Add(obs_sceneitem_t *item) -{ - if (obs_sceneitem_is_group(item)) { - SceneChanged(); - } else { - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, item); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), item); - } -} - -void SourceTreeModel::Remove(obs_sceneitem_t *item) -{ - int idx = -1; - for (int i = 0; i < items.count(); i++) { - if (items[i] == item) { - idx = i; - break; - } - } - - if (idx == -1) - return; - - int startIdx = idx; - int endIdx = idx; - - bool is_group = obs_sceneitem_is_group(item); - if (is_group) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = endIdx + 1; i < items.count(); i++) { - obs_sceneitem_t *subitem = items[i]; - obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); - - if (subscene == scene) - endIdx = i; - else - break; - } - } - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(idx, endIdx - startIdx + 1); - endRemoveRows(); - - if (is_group) - UpdateGroupState(true); - - OBSBasic::Get()->UpdateContextBarDeferred(); -} - -OBSSceneItem SourceTreeModel::Get(int idx) -{ - if (idx == -1 || idx >= items.count()) - return OBSSceneItem(); - return items[idx]; -} - -SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) -{ - obs_frontend_add_event_callback(OBSFrontendEvent, this); -} - -int SourceTreeModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : items.count(); -} - -QVariant SourceTreeModel::data(const QModelIndex &index, int role) const -{ - if (role == Qt::AccessibleTextRole) { - OBSSceneItem item = items[index.row()]; - obs_source_t *source = obs_sceneitem_get_source(item); - return QVariant(QT_UTF8(obs_source_get_name(source))); - } - - return QVariant(); -} - -Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; - - obs_sceneitem_t *item = items[index.row()]; - bool is_group = obs_sceneitem_is_group(item); - - return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | - (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); -} - -Qt::DropActions SourceTreeModel::supportedDropActions() const -{ - return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; -} - -QString SourceTreeModel::GetNewGroupName() -{ - OBSScene scene = GetCurrentScene(); - QString name = QTStr("Group"); - - int i = 2; - for (;;) { - OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); - if (!group) - break; - name = QTStr("Basic.Main.Group").arg(QString::number(i++)); - } - - return name; -} - -void SourceTreeModel::AddGroup() -{ - QString name = GetNewGroupName(); - obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); - if (!group) - return; - - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, group); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), group); - UpdateGroupState(true); - - QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); -} - -void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) -{ - if (indices.count() == 0) - return; - - OBSBasic *main = OBSBasic::Get(); - OBSScene scene = GetCurrentScene(); - QString name = GetNewGroupName(); - - QVector item_order; - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - item_order << item; - } - - st->undoSceneData = main->BackupScene(scene); - - obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); - if (!item) { - st->undoSceneData = nullptr; - return; - } - - main->undo_s.push_disabled(); - - for (obs_sceneitem_t *item : item_order) - obs_sceneitem_select(item, false); - - hasGroups = true; - st->UpdateWidgets(true); - - obs_sceneitem_select(item, true); - - /* ----------------------------------------------------------------- */ - /* obs_scene_insert_group triggers a full refresh of scene items via */ - /* the item_add signal. No need to insert a row, just edit the one */ - /* that's created automatically. */ - - int newIdx = indices[0].row(); - QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); -} - -void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) -{ - OBSBasic *main = OBSBasic::Get(); - if (indices.count() == 0) - return; - - OBSScene scene = main->GetCurrentScene(); - OBSData undoData = main->BackupScene(scene); - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_sceneitem_group_ungroup(item); - } - - SceneChanged(); - - OBSData redoData = main->BackupScene(scene); - main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); -} - -void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) -{ - int itemIdx = items.indexOf(item); - if (itemIdx == -1) - return; - - itemIdx++; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - QVector subItems; - obs_scene_enum_items(scene, enumItem, &subItems); - - if (!subItems.size()) - return; - - beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); - for (int i = 0; i < subItems.size(); i++) - items.insert(i + itemIdx, subItems[i]); - endInsertRows(); - - st->UpdateWidgets(); -} - -void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) -{ - int startIdx = -1; - int endIdx = -1; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = 0; i < items.size(); i++) { - obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); - - if (itemScene == scene) { - if (startIdx == -1) - startIdx = i; - endIdx = i; - } - } - - if (startIdx == -1) - return; - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(startIdx, endIdx - startIdx + 1); - endRemoveRows(); -} - -void SourceTreeModel::UpdateGroupState(bool update) -{ - bool nowHasGroups = false; - for (auto &item : items) { - if (obs_sceneitem_is_group(item)) { - nowHasGroups = true; - break; - } - } - - if (nowHasGroups != hasGroups) { - hasGroups = nowHasGroups; - if (update) { - st->UpdateWidgets(true); - } - } -} - -/* ========================================================================= */ - SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) { SourceTreeModel *stm_ = new SourceTreeModel(this); @@ -1067,8 +107,6 @@ void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); } -Q_DECLARE_METATYPE(OBSSceneItem); - void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) @@ -1598,16 +636,3 @@ void SourceTree::paintEvent(QPaintEvent *event) QListView::paintEvent(event); } } - -SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - SourceTree *tree = qobject_cast(parent()); - QWidget *item = tree->indexWidget(index); - - if (!item) - return (QSize(0, 0)); - - return (QSize(option.widget->minimumWidth(), item->height())); -} diff --git a/frontend/components/SourceTree.hpp b/frontend/components/SourceTree.hpp index 8f8922d52..60cacaa3b 100644 --- a/frontend/components/SourceTree.hpp +++ b/frontend/components/SourceTree.hpp @@ -1,130 +1,11 @@ #pragma once -#include -#include -#include +#include "SourceTreeItem.hpp" +#include "SourceTreeModel.hpp" + #include -#include #include #include -#include -#include -#include -#include - -class QLabel; -class OBSSourceLabel; -class QCheckBox; -class QLineEdit; -class SourceTree; -class QSpacerItem; -class QHBoxLayout; -class VisibilityItemWidget; - -class SourceTreeItem : public QFrame { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeModel; - - void mouseDoubleClickEvent(QMouseEvent *event) override; - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual bool eventFilter(QObject *object, QEvent *event) override; - - void Update(bool force); - - enum class Type { - Unknown, - Item, - Group, - SubItem, - }; - - void DisconnectSignals(); - void ReconnectSignals(); - - Type type = Type::Unknown; - -public: - explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); - bool IsEditing(); - -private: - QSpacerItem *spacer = nullptr; - QCheckBox *expand = nullptr; - QLabel *iconLabel = nullptr; - QCheckBox *vis = nullptr; - QCheckBox *lock = nullptr; - QHBoxLayout *boxLayout = nullptr; - OBSSourceLabel *label = nullptr; - - QLineEdit *editor = nullptr; - - std::string newName; - - SourceTree *tree; - OBSSceneItem sceneitem; - std::vector sigs; - - virtual void paintEvent(QPaintEvent *event) override; - - void ExitEditModeInternal(bool save); - -private slots: - void Clear(); - - void EnterEditMode(); - void ExitEditMode(bool save); - - void VisibilityChanged(bool visible); - void LockedChanged(bool locked); - - void ExpandClicked(bool checked); - - void Select(); - void Deselect(); -}; - -class SourceTreeModel : public QAbstractListModel { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeItem; - - SourceTree *st; - QVector items; - bool hasGroups = false; - - static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); - void Clear(); - void SceneChanged(); - void ReorderItems(); - - void Add(obs_sceneitem_t *item); - void Remove(obs_sceneitem_t *item); - OBSSceneItem Get(int idx); - QString GetNewGroupName(); - void AddGroup(); - - void GroupSelectedItems(QModelIndexList &indices); - void UngroupSelectedGroups(QModelIndexList &indices); - - void ExpandGroup(obs_sceneitem_t *item); - void CollapseGroup(obs_sceneitem_t *item); - - void UpdateGroupState(bool update); - -public: - explicit SourceTreeModel(SourceTree *st); - - virtual int rowCount(const QModelIndex &parent) const override; - virtual QVariant data(const QModelIndex &index, int role) const override; - - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - virtual Qt::DropActions supportedDropActions() const override; -}; class SourceTree : public QListView { Q_OBJECT @@ -192,11 +73,3 @@ protected: virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; }; - -class SourceTreeDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SourceTreeDelegate(QObject *parent); - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; -}; diff --git a/frontend/components/SourceTreeDelegate.cpp b/frontend/components/SourceTreeDelegate.cpp index a0242e3cd..b849e99ec 100644 --- a/frontend/components/SourceTreeDelegate.cpp +++ b/frontend/components/SourceTreeDelegate.cpp @@ -1,1603 +1,6 @@ -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "source-tree.hpp" -#include "platform.hpp" -#include "source-label.hpp" - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -static inline OBSScene GetCurrentScene() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - return main->GetCurrentScene(); -} - -/* ========================================================================= */ - -SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) -{ - setAttribute(Qt::WA_TranslucentBackground); - setMouseTracking(true); - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - const char *name = obs_source_get_name(source); - - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); - int preset = obs_data_get_int(privData, "color-preset"); - - if (preset == 1) { - const char *color = obs_data_get_string(privData, "color"); - std::string col = "background: "; - col += color; - setStyleSheet(col.c_str()); - } else if (preset > 1) { - setStyleSheet(""); - setProperty("bgColor", preset - 1); - } else { - setStyleSheet("background: none"); - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *id = obs_source_get_id(source); - - bool sourceVisible = obs_sceneitem_visible(sceneitem); - - if (tree->iconsVisible) { - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = main->GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = main->GetGroupIcon(); - else - icon = main->GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - - iconLabel = new QLabel(); - iconLabel->setPixmap(pixmap); - iconLabel->setEnabled(sourceVisible); - iconLabel->setStyleSheet("background: none"); - iconLabel->setProperty("class", "source-icon"); - } - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setChecked(sourceVisible); - vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); - vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); - - lock = new QCheckBox(); - lock->setProperty("class", "checkbox-icon indicator-lock"); - lock->setChecked(obs_sceneitem_locked(sceneitem)); - lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); - lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - label->setAttribute(Qt::WA_TranslucentBackground); - label->setEnabled(sourceVisible); - -#ifdef __APPLE__ - vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); - lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - boxLayout = new QHBoxLayout(); - - boxLayout->setContentsMargins(0, 0, 0, 0); - boxLayout->setSpacing(0); - if (iconLabel) { - boxLayout->addWidget(iconLabel); - boxLayout->addSpacing(2); - } - boxLayout->addWidget(label); - boxLayout->addWidget(vis); - boxLayout->addWidget(lock); -#ifdef __APPLE__ - /* Hack: Fixes a bug where scrollbars would be above the lock icon */ - boxLayout->addSpacing(16); -#endif - - Update(false); - - setLayout(boxLayout); - - /* --------------------------------------------------------- */ - - auto setItemVisible = [this](bool val) { - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *scenesource = obs_scene_get_source(scene); - int64_t id = obs_sceneitem_get_id(sceneitem); - const char *name = obs_source_get_name(scenesource); - const char *uuid = obs_source_get_uuid(scenesource); - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - - auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { - OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); - obs_scene_t *sc = obs_group_or_scene_from_source(s); - obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); - if (si) - obs_sceneitem_set_visible(si, val); - }; - - QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); - - OBSBasic *main = OBSBasic::Get(); - main->undo_s.add_action(str.arg(obs_source_get_name(source), name), - std::bind(undo_redo, std::placeholders::_1, id, !val), - std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); - - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_visible(sceneitem, val); - }; - - auto setItemLocked = [this](bool checked) { - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_locked(sceneitem, checked); - }; - - connect(vis, &QAbstractButton::clicked, setItemVisible); - connect(lock, &QAbstractButton::clicked, setItemLocked); -} - -void SourceTreeItem::paintEvent(QPaintEvent *event) -{ - QStyleOption opt; - opt.initFrom(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - QWidget::paintEvent(event); -} - -void SourceTreeItem::DisconnectSignals() -{ - sigs.clear(); -} - -void SourceTreeItem::Clear() -{ - DisconnectSignals(); - sceneitem = nullptr; -} - -void SourceTreeItem::ReconnectSignals() -{ - if (!sceneitem) - return; - - DisconnectSignals(); - - /* --------------------------------------------------------- */ - - auto removeItem = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); - - if (curItem == this_->sceneitem) { - QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), - Q_ARG(OBSScene, curScene)); - curItem = nullptr; - } - if (!curItem) - QMetaObject::invokeMethod(this_, "Clear"); - }; - - auto itemVisible = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool visible = calldata_bool(cd, "visible"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); - }; - - auto itemLocked = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool locked = calldata_bool(cd, "locked"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); - }; - - auto itemSelect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Select"); - }; - - auto itemDeselect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Deselect"); - }; - - auto reorderGroup = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - QMetaObject::invokeMethod(this_->tree, "ReorderItems"); - }; - - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *sceneSource = obs_scene_get_source(scene); - signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); - - sigs.emplace_back(signal, "remove", removeItem, this); - sigs.emplace_back(signal, "item_remove", removeItem, this); - sigs.emplace_back(signal, "item_visible", itemVisible, this); - sigs.emplace_back(signal, "item_locked", itemLocked, this); - sigs.emplace_back(signal, "item_select", itemSelect, this); - sigs.emplace_back(signal, "item_deselect", itemDeselect, this); - - if (obs_sceneitem_is_group(sceneitem)) { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - - sigs.emplace_back(signal, "reorder", reorderGroup, this); - } - - /* --------------------------------------------------------- */ - - auto removeSource = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - this_->DisconnectSignals(); - this_->sceneitem = nullptr; - QMetaObject::invokeMethod(this_->tree, "RefreshItems"); - }; - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - sigs.emplace_back(signal, "remove", removeSource, this); -} - -void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); - - if (expand) { - expand->setChecked(!expand->isChecked()); - } else { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - if (obs_source_configurable(source)) { - main->CreatePropertiesWindow(source); - } - } -} - -void SourceTreeItem::enterEvent(QEnterEvent *event) -{ - QWidget::enterEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); - preview->hoveredPreviewItems.push_back(sceneitem); -} - -void SourceTreeItem::leaveEvent(QEvent *event) -{ - QWidget::leaveEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); -} - -bool SourceTreeItem::IsEditing() -{ - return editor != nullptr; -} - -void SourceTreeItem::EnterEditMode() -{ - setFocusPolicy(Qt::StrongFocus); - int index = boxLayout->indexOf(label); - boxLayout->removeWidget(label); - editor = new QLineEdit(label->text()); - editor->setStyleSheet("background: none"); - editor->selectAll(); - editor->installEventFilter(this); - boxLayout->insertWidget(index, editor); - setFocusProxy(editor); -} - -void SourceTreeItem::ExitEditMode(bool save) -{ - ExitEditModeInternal(save); - - if (tree->undoSceneData) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); - main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); - - tree->undoSceneData = nullptr; - } -} - -void SourceTreeItem::ExitEditModeInternal(bool save) -{ - if (!editor) { - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSScene scene = main->GetCurrentScene(); - - newName = QT_TO_UTF8(editor->text()); - - setFocusProxy(nullptr); - int index = boxLayout->indexOf(editor); - boxLayout->removeWidget(editor); - delete editor; - editor = nullptr; - setFocusPolicy(Qt::NoFocus); - boxLayout->insertWidget(index, label); - setFocus(); - - /* ----------------------------------------- */ - /* check for empty string */ - - if (!save) - return; - - if (newName.empty()) { - OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; - } - - /* ----------------------------------------- */ - /* Check for same name */ - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - if (newName == obs_source_get_name(source)) - return; - - /* ----------------------------------------- */ - /* check for existing source */ - - OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); - bool exists = !!existingSource; - - if (exists) { - OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - return; - } - - /* ----------------------------------------- */ - /* rename */ - - QSignalBlocker sourcesSignalBlocker(this); - std::string prevName(obs_source_get_name(source)); - std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); - auto undo = [scene_uuid, prevName, main](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prevName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - std::string editedName = newName; - - auto redo = [scene_uuid, main, editedName](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, editedName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - const char *uuid = obs_source_get_uuid(source); - main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); - - obs_source_set_name(source, newName.c_str()); -} - -bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) -{ - if (editor != object) - return false; - - if (LineEditCanceled(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); - return true; - } - if (LineEditChanged(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); - return true; - } - - return false; -} - -void SourceTreeItem::VisibilityChanged(bool visible) -{ - if (iconLabel) { - iconLabel->setEnabled(visible); - } - label->setEnabled(visible); - vis->setChecked(visible); -} - -void SourceTreeItem::LockedChanged(bool locked) -{ - lock->setChecked(locked); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Update(bool force) -{ - OBSScene scene = GetCurrentScene(); - obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); - - Type newType; - - /* ------------------------------------------------- */ - /* if it's a group item, insert group checkbox */ - - if (obs_sceneitem_is_group(sceneitem)) { - newType = Type::Group; - - /* ------------------------------------------------- */ - /* if it's a group sub-item */ - - } else if (itemScene != scene) { - newType = Type::SubItem; - - /* ------------------------------------------------- */ - /* if it's a regular item */ - - } else { - newType = Type::Item; - } - - /* ------------------------------------------------- */ - - if (!force && newType == type) { - return; - } - - /* ------------------------------------------------- */ - - ReconnectSignals(); - - if (spacer) { - boxLayout->removeItem(spacer); - delete spacer; - spacer = nullptr; - } - - if (type == Type::Group) { - boxLayout->removeWidget(expand); - expand->deleteLater(); - expand = nullptr; - } - - type = newType; - - if (type == Type::SubItem) { - spacer = new QSpacerItem(16, 1); - boxLayout->insertItem(0, spacer); - - } else if (type == Type::Group) { - expand = new QCheckBox(); - expand->setProperty("class", "checkbox-icon indicator-expand"); -#ifdef __APPLE__ - expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - boxLayout->insertWidget(0, expand); - - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - expand->blockSignals(true); - expand->setChecked(obs_data_get_bool(data, "collapsed")); - expand->blockSignals(false); - - connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); - - } else { - spacer = new QSpacerItem(3, 1); - boxLayout->insertItem(0, spacer); - } -} - -void SourceTreeItem::ExpandClicked(bool checked) -{ - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - - obs_data_set_bool(data, "collapsed", checked); - - if (!checked) - tree->GetStm()->ExpandGroup(sceneitem); - else - tree->GetStm()->CollapseGroup(sceneitem); -} - -void SourceTreeItem::Select() -{ - tree->SelectItem(sceneitem, true); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Deselect() -{ - tree->SelectItem(sceneitem, false); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -/* ========================================================================= */ - -void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) -{ - SourceTreeModel *stm = reinterpret_cast(ptr); - - switch (event) { - case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: - stm->SceneChanged(); - break; - case OBS_FRONTEND_EVENT_EXIT: - stm->Clear(); - obs_frontend_remove_event_callback(OBSFrontendEvent, stm); - break; - case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: - stm->Clear(); - break; - default: - break; - } -} - -void SourceTreeModel::Clear() -{ - beginResetModel(); - items.clear(); - endResetModel(); - - hasGroups = false; -} - -static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) -{ - QVector &items = *reinterpret_cast *>(ptr); - - obs_source_t *src = obs_sceneitem_get_source(item); - if (obs_source_removed(src)) { - return true; - } - - if (obs_sceneitem_is_group(item)) { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); - - bool collapse = obs_data_get_bool(data, "collapsed"); - if (!collapse) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - obs_scene_enum_items(scene, enumItem, &items); - } - } - - items.insert(0, item); - return true; -} - -void SourceTreeModel::SceneChanged() -{ - OBSScene scene = GetCurrentScene(); - - beginResetModel(); - items.clear(); - obs_scene_enum_items(scene, enumItem, &items); - endResetModel(); - - UpdateGroupState(false); - st->ResetWidgets(); - - for (int i = 0; i < items.count(); i++) { - bool select = obs_sceneitem_selected(items[i]); - QModelIndex index = createIndex(i, 0); - - st->selectionModel()->select(index, - select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); - } -} - -/* moves a scene item index (blame linux distros for using older Qt builds) */ -static inline void MoveItem(QVector &items, int oldIdx, int newIdx) -{ - OBSSceneItem item = items[oldIdx]; - items.remove(oldIdx); - items.insert(newIdx, item); -} - -/* reorders list optimally with model reorder funcs */ -void SourceTreeModel::ReorderItems() -{ - OBSScene scene = GetCurrentScene(); - - QVector newitems; - obs_scene_enum_items(scene, enumItem, &newitems); - - /* if item list has changed size, do full reset */ - if (newitems.count() != items.count()) { - SceneChanged(); - return; - } - - for (;;) { - int idx1Old = 0; - int idx1New = 0; - int count; - int i; - - /* find first starting changed item index */ - for (i = 0; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[i]; - obs_sceneitem_t *newItem = newitems[i]; - if (oldItem != newItem) { - idx1Old = i; - break; - } - } - - /* if everything is the same, break */ - if (i == newitems.count()) { - break; - } - - /* find new starting index */ - for (i = idx1Old + 1; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[idx1Old]; - obs_sceneitem_t *newItem = newitems[i]; - - if (oldItem == newItem) { - idx1New = i; - break; - } - } - - /* if item could not be found, do full reset */ - if (i == newitems.count()) { - SceneChanged(); - return; - } - - /* get move count */ - for (count = 1; (idx1New + count) < newitems.count(); count++) { - int oldIdx = idx1Old + count; - int newIdx = idx1New + count; - - obs_sceneitem_t *oldItem = items[oldIdx]; - obs_sceneitem_t *newItem = newitems[newIdx]; - - if (oldItem != newItem) { - break; - } - } - - /* move items */ - beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); - for (i = 0; i < count; i++) { - int to = idx1New + count; - if (to > idx1Old) - to--; - MoveItem(items, idx1Old, to); - } - endMoveRows(); - } -} - -void SourceTreeModel::Add(obs_sceneitem_t *item) -{ - if (obs_sceneitem_is_group(item)) { - SceneChanged(); - } else { - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, item); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), item); - } -} - -void SourceTreeModel::Remove(obs_sceneitem_t *item) -{ - int idx = -1; - for (int i = 0; i < items.count(); i++) { - if (items[i] == item) { - idx = i; - break; - } - } - - if (idx == -1) - return; - - int startIdx = idx; - int endIdx = idx; - - bool is_group = obs_sceneitem_is_group(item); - if (is_group) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = endIdx + 1; i < items.count(); i++) { - obs_sceneitem_t *subitem = items[i]; - obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); - - if (subscene == scene) - endIdx = i; - else - break; - } - } - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(idx, endIdx - startIdx + 1); - endRemoveRows(); - - if (is_group) - UpdateGroupState(true); - - OBSBasic::Get()->UpdateContextBarDeferred(); -} - -OBSSceneItem SourceTreeModel::Get(int idx) -{ - if (idx == -1 || idx >= items.count()) - return OBSSceneItem(); - return items[idx]; -} - -SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) -{ - obs_frontend_add_event_callback(OBSFrontendEvent, this); -} - -int SourceTreeModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : items.count(); -} - -QVariant SourceTreeModel::data(const QModelIndex &index, int role) const -{ - if (role == Qt::AccessibleTextRole) { - OBSSceneItem item = items[index.row()]; - obs_source_t *source = obs_sceneitem_get_source(item); - return QVariant(QT_UTF8(obs_source_get_name(source))); - } - - return QVariant(); -} - -Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; - - obs_sceneitem_t *item = items[index.row()]; - bool is_group = obs_sceneitem_is_group(item); - - return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | - (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); -} - -Qt::DropActions SourceTreeModel::supportedDropActions() const -{ - return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; -} - -QString SourceTreeModel::GetNewGroupName() -{ - OBSScene scene = GetCurrentScene(); - QString name = QTStr("Group"); - - int i = 2; - for (;;) { - OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); - if (!group) - break; - name = QTStr("Basic.Main.Group").arg(QString::number(i++)); - } - - return name; -} - -void SourceTreeModel::AddGroup() -{ - QString name = GetNewGroupName(); - obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); - if (!group) - return; - - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, group); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), group); - UpdateGroupState(true); - - QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); -} - -void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) -{ - if (indices.count() == 0) - return; - - OBSBasic *main = OBSBasic::Get(); - OBSScene scene = GetCurrentScene(); - QString name = GetNewGroupName(); - - QVector item_order; - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - item_order << item; - } - - st->undoSceneData = main->BackupScene(scene); - - obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); - if (!item) { - st->undoSceneData = nullptr; - return; - } - - main->undo_s.push_disabled(); - - for (obs_sceneitem_t *item : item_order) - obs_sceneitem_select(item, false); - - hasGroups = true; - st->UpdateWidgets(true); - - obs_sceneitem_select(item, true); - - /* ----------------------------------------------------------------- */ - /* obs_scene_insert_group triggers a full refresh of scene items via */ - /* the item_add signal. No need to insert a row, just edit the one */ - /* that's created automatically. */ - - int newIdx = indices[0].row(); - QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); -} - -void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) -{ - OBSBasic *main = OBSBasic::Get(); - if (indices.count() == 0) - return; - - OBSScene scene = main->GetCurrentScene(); - OBSData undoData = main->BackupScene(scene); - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_sceneitem_group_ungroup(item); - } - - SceneChanged(); - - OBSData redoData = main->BackupScene(scene); - main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); -} - -void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) -{ - int itemIdx = items.indexOf(item); - if (itemIdx == -1) - return; - - itemIdx++; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - QVector subItems; - obs_scene_enum_items(scene, enumItem, &subItems); - - if (!subItems.size()) - return; - - beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); - for (int i = 0; i < subItems.size(); i++) - items.insert(i + itemIdx, subItems[i]); - endInsertRows(); - - st->UpdateWidgets(); -} - -void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) -{ - int startIdx = -1; - int endIdx = -1; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = 0; i < items.size(); i++) { - obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); - - if (itemScene == scene) { - if (startIdx == -1) - startIdx = i; - endIdx = i; - } - } - - if (startIdx == -1) - return; - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(startIdx, endIdx - startIdx + 1); - endRemoveRows(); -} - -void SourceTreeModel::UpdateGroupState(bool update) -{ - bool nowHasGroups = false; - for (auto &item : items) { - if (obs_sceneitem_is_group(item)) { - nowHasGroups = true; - break; - } - } - - if (nowHasGroups != hasGroups) { - hasGroups = nowHasGroups; - if (update) { - st->UpdateWidgets(true); - } - } -} - -/* ========================================================================= */ - -SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) -{ - SourceTreeModel *stm_ = new SourceTreeModel(this); - setModel(stm_); - setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - UpdateNoSourcesMessage(); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); - - setItemDelegate(new SourceTreeDelegate(this)); -} - -void SourceTree::UpdateIcons() -{ - SourceTreeModel *stm = GetStm(); - stm->SceneChanged(); -} - -void SourceTree::SetIconsVisible(bool visible) -{ - SourceTreeModel *stm = GetStm(); - - iconsVisible = visible; - stm->SceneChanged(); -} - -void SourceTree::ResetWidgets() -{ - OBSScene scene = GetCurrentScene(); - - SourceTreeModel *stm = GetStm(); - stm->UpdateGroupState(false); - - for (int i = 0; i < stm->items.count(); i++) { - QModelIndex index = stm->createIndex(i, 0, nullptr); - setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); - } -} - -void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) -{ - setIndexWidget(idx, new SourceTreeItem(this, item)); -} - -void SourceTree::UpdateWidgets(bool force) -{ - SourceTreeModel *stm = GetStm(); - - for (int i = 0; i < stm->items.size(); i++) { - obs_sceneitem_t *item = stm->items[i]; - SourceTreeItem *widget = GetItemWidget(i); - - if (!widget) { - UpdateWidget(stm->createIndex(i, 0), item); - } else { - widget->Update(force); - } - } -} - -void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) -{ - SourceTreeModel *stm = GetStm(); - int i = 0; - - for (; i < stm->items.count(); i++) { - if (stm->items[i] == sceneitem) - break; - } - - if (i == stm->items.count()) - return; - - QModelIndex index = stm->createIndex(i, 0); - if (index.isValid() && select != selectionModel()->isSelected(index)) - selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); -} - -Q_DECLARE_METATYPE(OBSSceneItem); - -void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - QListView::mouseDoubleClickEvent(event); -} - -void SourceTree::dropEvent(QDropEvent *event) -{ - if (event->source() != this) { - QListView::dropEvent(event); - return; - } - - OBSBasic *main = OBSBasic::Get(); - - OBSScene scene = GetCurrentScene(); - obs_source_t *scenesource = obs_scene_get_source(scene); - SourceTreeModel *stm = GetStm(); - auto &items = stm->items; - QModelIndexList indices = selectedIndexes(); - - DropIndicatorPosition indicator = dropIndicatorPosition(); - int row = indexAt(event->position().toPoint()).row(); - bool emptyDrop = row == -1; - - if (emptyDrop) { - if (!items.size()) { - QListView::dropEvent(event); - return; - } - - row = items.size() - 1; - indicator = QAbstractItemView::BelowItem; - } - - /* --------------------------------------- */ - /* store destination group if moving to a */ - /* group */ - - obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ - bool itemIsGroup = obs_sceneitem_is_group(dropItem); - - obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); - - /* not a group if moving above the group */ - if (indicator == QAbstractItemView::AboveItem && itemIsGroup) - dropGroup = nullptr; - if (emptyDrop) - dropGroup = nullptr; - - /* --------------------------------------- */ - /* remember to remove list items if */ - /* dropping on collapsed group */ - - bool dropOnCollapsed = false; - if (dropGroup) { - obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); - dropOnCollapsed = obs_data_get_bool(data, "collapsed"); - obs_data_release(data); - } - - if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || - indicator == QAbstractItemView::OnViewport) - row++; - - if (row < 0 || row > stm->items.count()) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* determine if any base group is selected */ - - bool hasGroups = false; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_is_group(item)) { - hasGroups = true; - break; - } - } - - /* --------------------------------------- */ - /* if dropping a group, detect if it's */ - /* below another group */ - - obs_sceneitem_t *itemBelow; - if (row == stm->items.count()) - itemBelow = nullptr; - else - itemBelow = stm->items[row]; - - if (hasGroups) { - if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { - dropGroup = nullptr; - dropOnCollapsed = false; - } - } - - /* --------------------------------------- */ - /* if dropping groups on other groups, */ - /* disregard as invalid drag/drop */ - - if (dropGroup && hasGroups) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* save undo data */ - std::vector sources; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_get_scene(item) != scene) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - } - if (dropGroup) - sources.push_back(obs_sceneitem_get_source(dropGroup)); - OBSData undo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* if selection includes base group items, */ - /* include all group sub-items and treat */ - /* them all as one */ - - if (hasGroups) { - /* remove sub-items if selected */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_scene_t *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - indices.removeAt(i); - } - } - - /* add all sub-items of selected groups */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - - if (obs_sceneitem_is_group(item)) { - for (int j = items.size() - 1; j >= 0; j--) { - obs_sceneitem_t *subitem = items[j]; - obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); - - if (subitemGroup == item) { - QModelIndex idx = stm->createIndex(j, 0); - indices.insert(i + 1, idx); - } - } - } - } - } - - /* --------------------------------------- */ - /* build persistent indices */ - - QList persistentIndices; - persistentIndices.reserve(indices.count()); - for (QModelIndex &index : indices) - persistentIndices.append(index); - std::sort(persistentIndices.begin(), persistentIndices.end()); - - /* --------------------------------------- */ - /* move all items to destination index */ - - int r = row; - for (auto &persistentIdx : persistentIndices) { - int from = persistentIdx.row(); - int to = r; - int itemTo = to; - - if (itemTo > from) - itemTo--; - - if (itemTo != from) { - stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); - MoveItem(items, from, itemTo); - stm->endMoveRows(); - } - - r = persistentIdx.row() + 1; - } - - std::sort(persistentIndices.begin(), persistentIndices.end()); - int firstIdx = persistentIndices.front().row(); - int lastIdx = persistentIndices.back().row(); - - /* --------------------------------------- */ - /* reorder scene items in back-end */ - - QVector orderList; - obs_sceneitem_t *lastGroup = nullptr; - int insertCollapsedIdx = 0; - - auto insertCollapsed = [&](obs_sceneitem_t *item) { - struct obs_sceneitem_order_info info; - info.group = lastGroup; - info.item = item; - - orderList.insert(insertCollapsedIdx++, info); - }; - - using insertCollapsed_t = decltype(insertCollapsed); - - auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - (*reinterpret_cast(param))(item); - return true; - }; - - auto insertLastGroup = [&]() { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); - bool collapsed = obs_data_get_bool(data, "collapsed"); - - if (collapsed) { - insertCollapsedIdx = 0; - obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); - } - - struct obs_sceneitem_order_info info; - info.group = nullptr; - info.item = lastGroup; - orderList.insert(0, info); - }; - - auto updateScene = [&]() { - struct obs_sceneitem_order_info info; - - for (int i = 0; i < items.size(); i++) { - obs_sceneitem_t *item = items[i]; - obs_sceneitem_t *group; - - if (obs_sceneitem_is_group(item)) { - if (lastGroup) { - insertLastGroup(); - } - lastGroup = item; - continue; - } - - if (!hasGroups && i >= firstIdx && i <= lastIdx) - group = dropGroup; - else - group = obs_sceneitem_get_group(scene, item); - - if (lastGroup && lastGroup != group) { - insertLastGroup(); - } - - lastGroup = group; - - info.group = group; - info.item = item; - orderList.insert(0, info); - } - - if (lastGroup) { - insertLastGroup(); - } - - obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); - }; - - using updateScene_t = decltype(updateScene); - - auto preUpdateScene = [](void *data, obs_scene_t *) { - (*reinterpret_cast(data))(); - }; - - ignoreReorder = true; - obs_scene_atomic_update(scene, preUpdateScene, &updateScene); - ignoreReorder = false; - - /* --------------------------------------- */ - /* save redo data */ - - OBSData redo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* add undo/redo action */ - - const char *scene_name = obs_source_get_name(scenesource); - QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); - main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); - - /* --------------------------------------- */ - /* remove items if dropped in to collapsed */ - /* group */ - - if (dropOnCollapsed) { - stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); - items.remove(firstIdx, lastIdx - firstIdx + 1); - stm->endRemoveRows(); - } - - /* --------------------------------------- */ - /* update widgets and accept event */ - - UpdateWidgets(true); - - event->accept(); - event->setDropAction(Qt::CopyAction); - - QListView::dropEvent(event); -} - -void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) -{ - { - QSignalBlocker sourcesSignalBlocker(this); - SourceTreeModel *stm = GetStm(); - - QModelIndexList selectedIdxs = selected.indexes(); - QModelIndexList deselectedIdxs = deselected.indexes(); - - for (int i = 0; i < selectedIdxs.count(); i++) { - int idx = selectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], true); - } - - for (int i = 0; i < deselectedIdxs.count(); i++) { - int idx = deselectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], false); - } - } - QListView::selectionChanged(selected, deselected); -} - -void SourceTree::NewGroupEdit(int row) -{ - if (!Edit(row)) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " - "code should never be reached.\nAnd by " - "\"never be reached\", I mean that " - "theoretically, it should be\nimpossible " - "for this code to be reached. But if this " - "code is reached,\nfeel free to laugh at " - "Lain, because apparently it is, in fact, " - "actually\npossible for this code to be " - "reached. But I mean, again, theoretically\n" - "it should be impossible. So if you see " - "this in your log, just know that\nit's " - "really dumb, and depressing. But at least " - "the undo/redo action is\nstill covered, so " - "in theory things *should* be fine. But " - "it's entirely\npossible that they might " - "not be exactly. But again, yea. This " - "really\nshould not be possible."); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg("Unknown"); - main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); - - undoSceneData = nullptr; - } -} - -bool SourceTree::Edit(int row) -{ - SourceTreeModel *stm = GetStm(); - if (row < 0 || row >= stm->items.count()) - return false; - - QModelIndex index = stm->createIndex(row, 0); - QWidget *widget = indexWidget(index); - SourceTreeItem *itemWidget = reinterpret_cast(widget); - if (itemWidget->IsEditing()) { -#ifdef __APPLE__ - itemWidget->ExitEditMode(true); -#endif - return false; - } - - itemWidget->EnterEditMode(); - edit(index); - return true; -} - -bool SourceTree::MultipleBaseSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (obs_sceneitem_is_group(item)) { - return false; - } - - obs_scene *itemScene = obs_sceneitem_get_scene(item); - if (itemScene != scene) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (!obs_sceneitem_is_group(item)) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupedItemsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - OBSScene scene = GetCurrentScene(); - - if (!selectedIndices.size()) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - obs_scene *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - return true; - } - } - - return false; -} - -void SourceTree::Remove(OBSSceneItem item, OBSScene scene) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - GetStm()->Remove(item); - main->SaveProject(); - - if (!main->SavingDisabled()) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - } -} - -void SourceTree::GroupSelectedItems() -{ - QModelIndexList indices = selectedIndexes(); - std::sort(indices.begin(), indices.end()); - GetStm()->GroupSelectedItems(indices); -} - -void SourceTree::UngroupSelectedGroups() -{ - QModelIndexList indices = selectedIndexes(); - GetStm()->UngroupSelectedGroups(indices); -} - -void SourceTree::AddGroup() -{ - GetStm()->AddGroup(); -} - -void SourceTree::UpdateNoSourcesMessage() -{ - QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; - iconNoSources.load(file); - - QTextOption opt(Qt::AlignHCenter); - opt.setWrapMode(QTextOption::WordWrap); - textNoSources.setTextOption(opt); - textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); - - textPrepared = false; -} - -void SourceTree::paintEvent(QPaintEvent *event) -{ - SourceTreeModel *stm = GetStm(); - if (stm && !stm->items.count()) { - QPainter p(viewport()); - - if (!textPrepared) { - textNoSources.prepare(QTransform(), p.font()); - textPrepared = true; - } - - QRectF iconRect = iconNoSources.viewBoxF(); - iconRect.setSize(QSizeF(32.0, 32.0)); - - QSizeF iconSize = iconRect.size(); - QSizeF textSize = textNoSources.size(); - QSizeF thisSize = size(); - const qreal spacing = 16.0; - - qreal totalHeight = iconSize.height() + spacing + textSize.height(); - - qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; - qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; - iconRect.moveTo(std::round(x), std::round(y)); - iconNoSources.render(&p, iconRect); - - x = thisSize.width() / 2.0 - textSize.width() / 2.0; - y += spacing + iconSize.height(); - p.drawStaticText(x, y, textNoSources); - } else { - QListView::paintEvent(event); - } -} +#include "SourceTree.hpp" +#include "SourceTreeDelegate.hpp" +#include "moc_SourceTreeDelegate.cpp" SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} diff --git a/frontend/components/SourceTreeDelegate.hpp b/frontend/components/SourceTreeDelegate.hpp index 8f8922d52..c5489395f 100644 --- a/frontend/components/SourceTreeDelegate.hpp +++ b/frontend/components/SourceTreeDelegate.hpp @@ -1,197 +1,10 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include -#include - -class QLabel; -class OBSSourceLabel; -class QCheckBox; -class QLineEdit; -class SourceTree; -class QSpacerItem; -class QHBoxLayout; -class VisibilityItemWidget; - -class SourceTreeItem : public QFrame { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeModel; - - void mouseDoubleClickEvent(QMouseEvent *event) override; - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual bool eventFilter(QObject *object, QEvent *event) override; - - void Update(bool force); - - enum class Type { - Unknown, - Item, - Group, - SubItem, - }; - - void DisconnectSignals(); - void ReconnectSignals(); - - Type type = Type::Unknown; - -public: - explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); - bool IsEditing(); - -private: - QSpacerItem *spacer = nullptr; - QCheckBox *expand = nullptr; - QLabel *iconLabel = nullptr; - QCheckBox *vis = nullptr; - QCheckBox *lock = nullptr; - QHBoxLayout *boxLayout = nullptr; - OBSSourceLabel *label = nullptr; - - QLineEdit *editor = nullptr; - - std::string newName; - - SourceTree *tree; - OBSSceneItem sceneitem; - std::vector sigs; - - virtual void paintEvent(QPaintEvent *event) override; - - void ExitEditModeInternal(bool save); - -private slots: - void Clear(); - - void EnterEditMode(); - void ExitEditMode(bool save); - - void VisibilityChanged(bool visible); - void LockedChanged(bool locked); - - void ExpandClicked(bool checked); - - void Select(); - void Deselect(); -}; - -class SourceTreeModel : public QAbstractListModel { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeItem; - - SourceTree *st; - QVector items; - bool hasGroups = false; - - static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); - void Clear(); - void SceneChanged(); - void ReorderItems(); - - void Add(obs_sceneitem_t *item); - void Remove(obs_sceneitem_t *item); - OBSSceneItem Get(int idx); - QString GetNewGroupName(); - void AddGroup(); - - void GroupSelectedItems(QModelIndexList &indices); - void UngroupSelectedGroups(QModelIndexList &indices); - - void ExpandGroup(obs_sceneitem_t *item); - void CollapseGroup(obs_sceneitem_t *item); - - void UpdateGroupState(bool update); - -public: - explicit SourceTreeModel(SourceTree *st); - - virtual int rowCount(const QModelIndex &parent) const override; - virtual QVariant data(const QModelIndex &index, int role) const override; - - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - virtual Qt::DropActions supportedDropActions() const override; -}; - -class SourceTree : public QListView { - Q_OBJECT - - bool ignoreReorder = false; - - friend class SourceTreeModel; - friend class SourceTreeItem; - - bool textPrepared = false; - QStaticText textNoSources; - QSvgRenderer iconNoSources; - - OBSData undoSceneData; - - bool iconsVisible = true; - - void UpdateNoSourcesMessage(); - - void ResetWidgets(); - void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); - void UpdateWidgets(bool force = false); - - inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } - -public: - inline SourceTreeItem *GetItemWidget(int idx) - { - QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); - return reinterpret_cast(widget); - } - - explicit SourceTree(QWidget *parent = nullptr); - - inline bool IgnoreReorder() const { return ignoreReorder; } - inline void Clear() { GetStm()->Clear(); } - - inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } - inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } - inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } - - void SelectItem(obs_sceneitem_t *sceneitem, bool select); - - bool MultipleBaseSelected() const; - bool GroupsSelected() const; - bool GroupedItemsSelected() const; - - void UpdateIcons(); - void SetIconsVisible(bool visible); - -public slots: - inline void ReorderItems() { GetStm()->ReorderItems(); } - inline void RefreshItems() { GetStm()->SceneChanged(); } - void Remove(OBSSceneItem item, OBSScene scene); - void GroupSelectedItems(); - void UngroupSelectedGroups(); - void AddGroup(); - bool Edit(int idx); - void NewGroupEdit(int idx); - -protected: - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void dropEvent(QDropEvent *event) override; - virtual void paintEvent(QPaintEvent *event) override; - - virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; -}; +#include class SourceTreeDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/components/SourceTreeItem.cpp b/frontend/components/SourceTreeItem.cpp index a0242e3cd..7b6b7a7f4 100644 --- a/frontend/components/SourceTreeItem.cpp +++ b/frontend/components/SourceTreeItem.cpp @@ -1,26 +1,15 @@ -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "source-tree.hpp" -#include "platform.hpp" -#include "source-label.hpp" +#include "SourceTreeItem.hpp" + +#include +#include #include -#include -#include -#include - -#include +#include #include -#include -#include -#include -#include -#include -#include +#include -#include -#include +#include "moc_SourceTreeItem.cpp" static inline OBSScene GetCurrentScene() { @@ -28,8 +17,6 @@ static inline OBSScene GetCurrentScene() return main->GetCurrentScene(); } -/* ========================================================================= */ - SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) { setAttribute(Qt::WA_TranslucentBackground); @@ -555,1059 +542,3 @@ void SourceTreeItem::Deselect() OBSBasic::Get()->UpdateContextBarDeferred(); OBSBasic::Get()->UpdateEditMenu(); } - -/* ========================================================================= */ - -void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) -{ - SourceTreeModel *stm = reinterpret_cast(ptr); - - switch (event) { - case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: - stm->SceneChanged(); - break; - case OBS_FRONTEND_EVENT_EXIT: - stm->Clear(); - obs_frontend_remove_event_callback(OBSFrontendEvent, stm); - break; - case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: - stm->Clear(); - break; - default: - break; - } -} - -void SourceTreeModel::Clear() -{ - beginResetModel(); - items.clear(); - endResetModel(); - - hasGroups = false; -} - -static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) -{ - QVector &items = *reinterpret_cast *>(ptr); - - obs_source_t *src = obs_sceneitem_get_source(item); - if (obs_source_removed(src)) { - return true; - } - - if (obs_sceneitem_is_group(item)) { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); - - bool collapse = obs_data_get_bool(data, "collapsed"); - if (!collapse) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - obs_scene_enum_items(scene, enumItem, &items); - } - } - - items.insert(0, item); - return true; -} - -void SourceTreeModel::SceneChanged() -{ - OBSScene scene = GetCurrentScene(); - - beginResetModel(); - items.clear(); - obs_scene_enum_items(scene, enumItem, &items); - endResetModel(); - - UpdateGroupState(false); - st->ResetWidgets(); - - for (int i = 0; i < items.count(); i++) { - bool select = obs_sceneitem_selected(items[i]); - QModelIndex index = createIndex(i, 0); - - st->selectionModel()->select(index, - select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); - } -} - -/* moves a scene item index (blame linux distros for using older Qt builds) */ -static inline void MoveItem(QVector &items, int oldIdx, int newIdx) -{ - OBSSceneItem item = items[oldIdx]; - items.remove(oldIdx); - items.insert(newIdx, item); -} - -/* reorders list optimally with model reorder funcs */ -void SourceTreeModel::ReorderItems() -{ - OBSScene scene = GetCurrentScene(); - - QVector newitems; - obs_scene_enum_items(scene, enumItem, &newitems); - - /* if item list has changed size, do full reset */ - if (newitems.count() != items.count()) { - SceneChanged(); - return; - } - - for (;;) { - int idx1Old = 0; - int idx1New = 0; - int count; - int i; - - /* find first starting changed item index */ - for (i = 0; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[i]; - obs_sceneitem_t *newItem = newitems[i]; - if (oldItem != newItem) { - idx1Old = i; - break; - } - } - - /* if everything is the same, break */ - if (i == newitems.count()) { - break; - } - - /* find new starting index */ - for (i = idx1Old + 1; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[idx1Old]; - obs_sceneitem_t *newItem = newitems[i]; - - if (oldItem == newItem) { - idx1New = i; - break; - } - } - - /* if item could not be found, do full reset */ - if (i == newitems.count()) { - SceneChanged(); - return; - } - - /* get move count */ - for (count = 1; (idx1New + count) < newitems.count(); count++) { - int oldIdx = idx1Old + count; - int newIdx = idx1New + count; - - obs_sceneitem_t *oldItem = items[oldIdx]; - obs_sceneitem_t *newItem = newitems[newIdx]; - - if (oldItem != newItem) { - break; - } - } - - /* move items */ - beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); - for (i = 0; i < count; i++) { - int to = idx1New + count; - if (to > idx1Old) - to--; - MoveItem(items, idx1Old, to); - } - endMoveRows(); - } -} - -void SourceTreeModel::Add(obs_sceneitem_t *item) -{ - if (obs_sceneitem_is_group(item)) { - SceneChanged(); - } else { - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, item); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), item); - } -} - -void SourceTreeModel::Remove(obs_sceneitem_t *item) -{ - int idx = -1; - for (int i = 0; i < items.count(); i++) { - if (items[i] == item) { - idx = i; - break; - } - } - - if (idx == -1) - return; - - int startIdx = idx; - int endIdx = idx; - - bool is_group = obs_sceneitem_is_group(item); - if (is_group) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = endIdx + 1; i < items.count(); i++) { - obs_sceneitem_t *subitem = items[i]; - obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); - - if (subscene == scene) - endIdx = i; - else - break; - } - } - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(idx, endIdx - startIdx + 1); - endRemoveRows(); - - if (is_group) - UpdateGroupState(true); - - OBSBasic::Get()->UpdateContextBarDeferred(); -} - -OBSSceneItem SourceTreeModel::Get(int idx) -{ - if (idx == -1 || idx >= items.count()) - return OBSSceneItem(); - return items[idx]; -} - -SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) -{ - obs_frontend_add_event_callback(OBSFrontendEvent, this); -} - -int SourceTreeModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : items.count(); -} - -QVariant SourceTreeModel::data(const QModelIndex &index, int role) const -{ - if (role == Qt::AccessibleTextRole) { - OBSSceneItem item = items[index.row()]; - obs_source_t *source = obs_sceneitem_get_source(item); - return QVariant(QT_UTF8(obs_source_get_name(source))); - } - - return QVariant(); -} - -Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; - - obs_sceneitem_t *item = items[index.row()]; - bool is_group = obs_sceneitem_is_group(item); - - return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | - (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); -} - -Qt::DropActions SourceTreeModel::supportedDropActions() const -{ - return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; -} - -QString SourceTreeModel::GetNewGroupName() -{ - OBSScene scene = GetCurrentScene(); - QString name = QTStr("Group"); - - int i = 2; - for (;;) { - OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); - if (!group) - break; - name = QTStr("Basic.Main.Group").arg(QString::number(i++)); - } - - return name; -} - -void SourceTreeModel::AddGroup() -{ - QString name = GetNewGroupName(); - obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); - if (!group) - return; - - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, group); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), group); - UpdateGroupState(true); - - QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); -} - -void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) -{ - if (indices.count() == 0) - return; - - OBSBasic *main = OBSBasic::Get(); - OBSScene scene = GetCurrentScene(); - QString name = GetNewGroupName(); - - QVector item_order; - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - item_order << item; - } - - st->undoSceneData = main->BackupScene(scene); - - obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); - if (!item) { - st->undoSceneData = nullptr; - return; - } - - main->undo_s.push_disabled(); - - for (obs_sceneitem_t *item : item_order) - obs_sceneitem_select(item, false); - - hasGroups = true; - st->UpdateWidgets(true); - - obs_sceneitem_select(item, true); - - /* ----------------------------------------------------------------- */ - /* obs_scene_insert_group triggers a full refresh of scene items via */ - /* the item_add signal. No need to insert a row, just edit the one */ - /* that's created automatically. */ - - int newIdx = indices[0].row(); - QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); -} - -void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) -{ - OBSBasic *main = OBSBasic::Get(); - if (indices.count() == 0) - return; - - OBSScene scene = main->GetCurrentScene(); - OBSData undoData = main->BackupScene(scene); - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_sceneitem_group_ungroup(item); - } - - SceneChanged(); - - OBSData redoData = main->BackupScene(scene); - main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); -} - -void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) -{ - int itemIdx = items.indexOf(item); - if (itemIdx == -1) - return; - - itemIdx++; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - QVector subItems; - obs_scene_enum_items(scene, enumItem, &subItems); - - if (!subItems.size()) - return; - - beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); - for (int i = 0; i < subItems.size(); i++) - items.insert(i + itemIdx, subItems[i]); - endInsertRows(); - - st->UpdateWidgets(); -} - -void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) -{ - int startIdx = -1; - int endIdx = -1; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = 0; i < items.size(); i++) { - obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); - - if (itemScene == scene) { - if (startIdx == -1) - startIdx = i; - endIdx = i; - } - } - - if (startIdx == -1) - return; - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(startIdx, endIdx - startIdx + 1); - endRemoveRows(); -} - -void SourceTreeModel::UpdateGroupState(bool update) -{ - bool nowHasGroups = false; - for (auto &item : items) { - if (obs_sceneitem_is_group(item)) { - nowHasGroups = true; - break; - } - } - - if (nowHasGroups != hasGroups) { - hasGroups = nowHasGroups; - if (update) { - st->UpdateWidgets(true); - } - } -} - -/* ========================================================================= */ - -SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) -{ - SourceTreeModel *stm_ = new SourceTreeModel(this); - setModel(stm_); - setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - UpdateNoSourcesMessage(); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); - - setItemDelegate(new SourceTreeDelegate(this)); -} - -void SourceTree::UpdateIcons() -{ - SourceTreeModel *stm = GetStm(); - stm->SceneChanged(); -} - -void SourceTree::SetIconsVisible(bool visible) -{ - SourceTreeModel *stm = GetStm(); - - iconsVisible = visible; - stm->SceneChanged(); -} - -void SourceTree::ResetWidgets() -{ - OBSScene scene = GetCurrentScene(); - - SourceTreeModel *stm = GetStm(); - stm->UpdateGroupState(false); - - for (int i = 0; i < stm->items.count(); i++) { - QModelIndex index = stm->createIndex(i, 0, nullptr); - setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); - } -} - -void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) -{ - setIndexWidget(idx, new SourceTreeItem(this, item)); -} - -void SourceTree::UpdateWidgets(bool force) -{ - SourceTreeModel *stm = GetStm(); - - for (int i = 0; i < stm->items.size(); i++) { - obs_sceneitem_t *item = stm->items[i]; - SourceTreeItem *widget = GetItemWidget(i); - - if (!widget) { - UpdateWidget(stm->createIndex(i, 0), item); - } else { - widget->Update(force); - } - } -} - -void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) -{ - SourceTreeModel *stm = GetStm(); - int i = 0; - - for (; i < stm->items.count(); i++) { - if (stm->items[i] == sceneitem) - break; - } - - if (i == stm->items.count()) - return; - - QModelIndex index = stm->createIndex(i, 0); - if (index.isValid() && select != selectionModel()->isSelected(index)) - selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); -} - -Q_DECLARE_METATYPE(OBSSceneItem); - -void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - QListView::mouseDoubleClickEvent(event); -} - -void SourceTree::dropEvent(QDropEvent *event) -{ - if (event->source() != this) { - QListView::dropEvent(event); - return; - } - - OBSBasic *main = OBSBasic::Get(); - - OBSScene scene = GetCurrentScene(); - obs_source_t *scenesource = obs_scene_get_source(scene); - SourceTreeModel *stm = GetStm(); - auto &items = stm->items; - QModelIndexList indices = selectedIndexes(); - - DropIndicatorPosition indicator = dropIndicatorPosition(); - int row = indexAt(event->position().toPoint()).row(); - bool emptyDrop = row == -1; - - if (emptyDrop) { - if (!items.size()) { - QListView::dropEvent(event); - return; - } - - row = items.size() - 1; - indicator = QAbstractItemView::BelowItem; - } - - /* --------------------------------------- */ - /* store destination group if moving to a */ - /* group */ - - obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ - bool itemIsGroup = obs_sceneitem_is_group(dropItem); - - obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); - - /* not a group if moving above the group */ - if (indicator == QAbstractItemView::AboveItem && itemIsGroup) - dropGroup = nullptr; - if (emptyDrop) - dropGroup = nullptr; - - /* --------------------------------------- */ - /* remember to remove list items if */ - /* dropping on collapsed group */ - - bool dropOnCollapsed = false; - if (dropGroup) { - obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); - dropOnCollapsed = obs_data_get_bool(data, "collapsed"); - obs_data_release(data); - } - - if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || - indicator == QAbstractItemView::OnViewport) - row++; - - if (row < 0 || row > stm->items.count()) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* determine if any base group is selected */ - - bool hasGroups = false; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_is_group(item)) { - hasGroups = true; - break; - } - } - - /* --------------------------------------- */ - /* if dropping a group, detect if it's */ - /* below another group */ - - obs_sceneitem_t *itemBelow; - if (row == stm->items.count()) - itemBelow = nullptr; - else - itemBelow = stm->items[row]; - - if (hasGroups) { - if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { - dropGroup = nullptr; - dropOnCollapsed = false; - } - } - - /* --------------------------------------- */ - /* if dropping groups on other groups, */ - /* disregard as invalid drag/drop */ - - if (dropGroup && hasGroups) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* save undo data */ - std::vector sources; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_get_scene(item) != scene) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - } - if (dropGroup) - sources.push_back(obs_sceneitem_get_source(dropGroup)); - OBSData undo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* if selection includes base group items, */ - /* include all group sub-items and treat */ - /* them all as one */ - - if (hasGroups) { - /* remove sub-items if selected */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_scene_t *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - indices.removeAt(i); - } - } - - /* add all sub-items of selected groups */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - - if (obs_sceneitem_is_group(item)) { - for (int j = items.size() - 1; j >= 0; j--) { - obs_sceneitem_t *subitem = items[j]; - obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); - - if (subitemGroup == item) { - QModelIndex idx = stm->createIndex(j, 0); - indices.insert(i + 1, idx); - } - } - } - } - } - - /* --------------------------------------- */ - /* build persistent indices */ - - QList persistentIndices; - persistentIndices.reserve(indices.count()); - for (QModelIndex &index : indices) - persistentIndices.append(index); - std::sort(persistentIndices.begin(), persistentIndices.end()); - - /* --------------------------------------- */ - /* move all items to destination index */ - - int r = row; - for (auto &persistentIdx : persistentIndices) { - int from = persistentIdx.row(); - int to = r; - int itemTo = to; - - if (itemTo > from) - itemTo--; - - if (itemTo != from) { - stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); - MoveItem(items, from, itemTo); - stm->endMoveRows(); - } - - r = persistentIdx.row() + 1; - } - - std::sort(persistentIndices.begin(), persistentIndices.end()); - int firstIdx = persistentIndices.front().row(); - int lastIdx = persistentIndices.back().row(); - - /* --------------------------------------- */ - /* reorder scene items in back-end */ - - QVector orderList; - obs_sceneitem_t *lastGroup = nullptr; - int insertCollapsedIdx = 0; - - auto insertCollapsed = [&](obs_sceneitem_t *item) { - struct obs_sceneitem_order_info info; - info.group = lastGroup; - info.item = item; - - orderList.insert(insertCollapsedIdx++, info); - }; - - using insertCollapsed_t = decltype(insertCollapsed); - - auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - (*reinterpret_cast(param))(item); - return true; - }; - - auto insertLastGroup = [&]() { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); - bool collapsed = obs_data_get_bool(data, "collapsed"); - - if (collapsed) { - insertCollapsedIdx = 0; - obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); - } - - struct obs_sceneitem_order_info info; - info.group = nullptr; - info.item = lastGroup; - orderList.insert(0, info); - }; - - auto updateScene = [&]() { - struct obs_sceneitem_order_info info; - - for (int i = 0; i < items.size(); i++) { - obs_sceneitem_t *item = items[i]; - obs_sceneitem_t *group; - - if (obs_sceneitem_is_group(item)) { - if (lastGroup) { - insertLastGroup(); - } - lastGroup = item; - continue; - } - - if (!hasGroups && i >= firstIdx && i <= lastIdx) - group = dropGroup; - else - group = obs_sceneitem_get_group(scene, item); - - if (lastGroup && lastGroup != group) { - insertLastGroup(); - } - - lastGroup = group; - - info.group = group; - info.item = item; - orderList.insert(0, info); - } - - if (lastGroup) { - insertLastGroup(); - } - - obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); - }; - - using updateScene_t = decltype(updateScene); - - auto preUpdateScene = [](void *data, obs_scene_t *) { - (*reinterpret_cast(data))(); - }; - - ignoreReorder = true; - obs_scene_atomic_update(scene, preUpdateScene, &updateScene); - ignoreReorder = false; - - /* --------------------------------------- */ - /* save redo data */ - - OBSData redo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* add undo/redo action */ - - const char *scene_name = obs_source_get_name(scenesource); - QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); - main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); - - /* --------------------------------------- */ - /* remove items if dropped in to collapsed */ - /* group */ - - if (dropOnCollapsed) { - stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); - items.remove(firstIdx, lastIdx - firstIdx + 1); - stm->endRemoveRows(); - } - - /* --------------------------------------- */ - /* update widgets and accept event */ - - UpdateWidgets(true); - - event->accept(); - event->setDropAction(Qt::CopyAction); - - QListView::dropEvent(event); -} - -void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) -{ - { - QSignalBlocker sourcesSignalBlocker(this); - SourceTreeModel *stm = GetStm(); - - QModelIndexList selectedIdxs = selected.indexes(); - QModelIndexList deselectedIdxs = deselected.indexes(); - - for (int i = 0; i < selectedIdxs.count(); i++) { - int idx = selectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], true); - } - - for (int i = 0; i < deselectedIdxs.count(); i++) { - int idx = deselectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], false); - } - } - QListView::selectionChanged(selected, deselected); -} - -void SourceTree::NewGroupEdit(int row) -{ - if (!Edit(row)) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " - "code should never be reached.\nAnd by " - "\"never be reached\", I mean that " - "theoretically, it should be\nimpossible " - "for this code to be reached. But if this " - "code is reached,\nfeel free to laugh at " - "Lain, because apparently it is, in fact, " - "actually\npossible for this code to be " - "reached. But I mean, again, theoretically\n" - "it should be impossible. So if you see " - "this in your log, just know that\nit's " - "really dumb, and depressing. But at least " - "the undo/redo action is\nstill covered, so " - "in theory things *should* be fine. But " - "it's entirely\npossible that they might " - "not be exactly. But again, yea. This " - "really\nshould not be possible."); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg("Unknown"); - main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); - - undoSceneData = nullptr; - } -} - -bool SourceTree::Edit(int row) -{ - SourceTreeModel *stm = GetStm(); - if (row < 0 || row >= stm->items.count()) - return false; - - QModelIndex index = stm->createIndex(row, 0); - QWidget *widget = indexWidget(index); - SourceTreeItem *itemWidget = reinterpret_cast(widget); - if (itemWidget->IsEditing()) { -#ifdef __APPLE__ - itemWidget->ExitEditMode(true); -#endif - return false; - } - - itemWidget->EnterEditMode(); - edit(index); - return true; -} - -bool SourceTree::MultipleBaseSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (obs_sceneitem_is_group(item)) { - return false; - } - - obs_scene *itemScene = obs_sceneitem_get_scene(item); - if (itemScene != scene) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (!obs_sceneitem_is_group(item)) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupedItemsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - OBSScene scene = GetCurrentScene(); - - if (!selectedIndices.size()) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - obs_scene *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - return true; - } - } - - return false; -} - -void SourceTree::Remove(OBSSceneItem item, OBSScene scene) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - GetStm()->Remove(item); - main->SaveProject(); - - if (!main->SavingDisabled()) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - } -} - -void SourceTree::GroupSelectedItems() -{ - QModelIndexList indices = selectedIndexes(); - std::sort(indices.begin(), indices.end()); - GetStm()->GroupSelectedItems(indices); -} - -void SourceTree::UngroupSelectedGroups() -{ - QModelIndexList indices = selectedIndexes(); - GetStm()->UngroupSelectedGroups(indices); -} - -void SourceTree::AddGroup() -{ - GetStm()->AddGroup(); -} - -void SourceTree::UpdateNoSourcesMessage() -{ - QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; - iconNoSources.load(file); - - QTextOption opt(Qt::AlignHCenter); - opt.setWrapMode(QTextOption::WordWrap); - textNoSources.setTextOption(opt); - textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); - - textPrepared = false; -} - -void SourceTree::paintEvent(QPaintEvent *event) -{ - SourceTreeModel *stm = GetStm(); - if (stm && !stm->items.count()) { - QPainter p(viewport()); - - if (!textPrepared) { - textNoSources.prepare(QTransform(), p.font()); - textPrepared = true; - } - - QRectF iconRect = iconNoSources.viewBoxF(); - iconRect.setSize(QSizeF(32.0, 32.0)); - - QSizeF iconSize = iconRect.size(); - QSizeF textSize = textNoSources.size(); - QSizeF thisSize = size(); - const qreal spacing = 16.0; - - qreal totalHeight = iconSize.height() + spacing + textSize.height(); - - qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; - qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; - iconRect.moveTo(std::round(x), std::round(y)); - iconNoSources.render(&p, iconRect); - - x = thisSize.width() / 2.0 - textSize.width() / 2.0; - y += spacing + iconSize.height(); - p.drawStaticText(x, y, textNoSources); - } else { - QListView::paintEvent(event); - } -} - -SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - SourceTree *tree = qobject_cast(parent()); - QWidget *item = tree->indexWidget(index); - - if (!item) - return (QSize(0, 0)); - - return (QSize(option.widget->minimumWidth(), item->height())); -} diff --git a/frontend/components/SourceTreeItem.hpp b/frontend/components/SourceTreeItem.hpp index 8f8922d52..38f981882 100644 --- a/frontend/components/SourceTreeItem.hpp +++ b/frontend/components/SourceTreeItem.hpp @@ -1,25 +1,17 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -class QLabel; -class OBSSourceLabel; -class QCheckBox; -class QLineEdit; -class SourceTree; +#include + class QSpacerItem; +class QCheckBox; +class QLabel; class QHBoxLayout; -class VisibilityItemWidget; +class OBSSourceLabel; +class QLineEdit; + +class SourceTree; class SourceTreeItem : public QFrame { Q_OBJECT @@ -86,117 +78,3 @@ private slots: void Select(); void Deselect(); }; - -class SourceTreeModel : public QAbstractListModel { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeItem; - - SourceTree *st; - QVector items; - bool hasGroups = false; - - static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); - void Clear(); - void SceneChanged(); - void ReorderItems(); - - void Add(obs_sceneitem_t *item); - void Remove(obs_sceneitem_t *item); - OBSSceneItem Get(int idx); - QString GetNewGroupName(); - void AddGroup(); - - void GroupSelectedItems(QModelIndexList &indices); - void UngroupSelectedGroups(QModelIndexList &indices); - - void ExpandGroup(obs_sceneitem_t *item); - void CollapseGroup(obs_sceneitem_t *item); - - void UpdateGroupState(bool update); - -public: - explicit SourceTreeModel(SourceTree *st); - - virtual int rowCount(const QModelIndex &parent) const override; - virtual QVariant data(const QModelIndex &index, int role) const override; - - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - virtual Qt::DropActions supportedDropActions() const override; -}; - -class SourceTree : public QListView { - Q_OBJECT - - bool ignoreReorder = false; - - friend class SourceTreeModel; - friend class SourceTreeItem; - - bool textPrepared = false; - QStaticText textNoSources; - QSvgRenderer iconNoSources; - - OBSData undoSceneData; - - bool iconsVisible = true; - - void UpdateNoSourcesMessage(); - - void ResetWidgets(); - void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); - void UpdateWidgets(bool force = false); - - inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } - -public: - inline SourceTreeItem *GetItemWidget(int idx) - { - QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); - return reinterpret_cast(widget); - } - - explicit SourceTree(QWidget *parent = nullptr); - - inline bool IgnoreReorder() const { return ignoreReorder; } - inline void Clear() { GetStm()->Clear(); } - - inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } - inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } - inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } - - void SelectItem(obs_sceneitem_t *sceneitem, bool select); - - bool MultipleBaseSelected() const; - bool GroupsSelected() const; - bool GroupedItemsSelected() const; - - void UpdateIcons(); - void SetIconsVisible(bool visible); - -public slots: - inline void ReorderItems() { GetStm()->ReorderItems(); } - inline void RefreshItems() { GetStm()->SceneChanged(); } - void Remove(OBSSceneItem item, OBSScene scene); - void GroupSelectedItems(); - void UngroupSelectedGroups(); - void AddGroup(); - bool Edit(int idx); - void NewGroupEdit(int idx); - -protected: - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void dropEvent(QDropEvent *event) override; - virtual void paintEvent(QPaintEvent *event) override; - - virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; -}; - -class SourceTreeDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SourceTreeDelegate(QObject *parent); - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; -}; diff --git a/frontend/components/SourceTreeModel.cpp b/frontend/components/SourceTreeModel.cpp index a0242e3cd..61e13d52b 100644 --- a/frontend/components/SourceTreeModel.cpp +++ b/frontend/components/SourceTreeModel.cpp @@ -1,26 +1,10 @@ -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "source-tree.hpp" -#include "platform.hpp" -#include "source-label.hpp" +#include "SourceTreeModel.hpp" + +#include #include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include +#include "moc_SourceTreeModel.cpp" static inline OBSScene GetCurrentScene() { @@ -28,536 +12,6 @@ static inline OBSScene GetCurrentScene() return main->GetCurrentScene(); } -/* ========================================================================= */ - -SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) -{ - setAttribute(Qt::WA_TranslucentBackground); - setMouseTracking(true); - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - const char *name = obs_source_get_name(source); - - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); - int preset = obs_data_get_int(privData, "color-preset"); - - if (preset == 1) { - const char *color = obs_data_get_string(privData, "color"); - std::string col = "background: "; - col += color; - setStyleSheet(col.c_str()); - } else if (preset > 1) { - setStyleSheet(""); - setProperty("bgColor", preset - 1); - } else { - setStyleSheet("background: none"); - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *id = obs_source_get_id(source); - - bool sourceVisible = obs_sceneitem_visible(sceneitem); - - if (tree->iconsVisible) { - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = main->GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = main->GetGroupIcon(); - else - icon = main->GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - - iconLabel = new QLabel(); - iconLabel->setPixmap(pixmap); - iconLabel->setEnabled(sourceVisible); - iconLabel->setStyleSheet("background: none"); - iconLabel->setProperty("class", "source-icon"); - } - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setChecked(sourceVisible); - vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); - vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); - - lock = new QCheckBox(); - lock->setProperty("class", "checkbox-icon indicator-lock"); - lock->setChecked(obs_sceneitem_locked(sceneitem)); - lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); - lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - label->setAttribute(Qt::WA_TranslucentBackground); - label->setEnabled(sourceVisible); - -#ifdef __APPLE__ - vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); - lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - boxLayout = new QHBoxLayout(); - - boxLayout->setContentsMargins(0, 0, 0, 0); - boxLayout->setSpacing(0); - if (iconLabel) { - boxLayout->addWidget(iconLabel); - boxLayout->addSpacing(2); - } - boxLayout->addWidget(label); - boxLayout->addWidget(vis); - boxLayout->addWidget(lock); -#ifdef __APPLE__ - /* Hack: Fixes a bug where scrollbars would be above the lock icon */ - boxLayout->addSpacing(16); -#endif - - Update(false); - - setLayout(boxLayout); - - /* --------------------------------------------------------- */ - - auto setItemVisible = [this](bool val) { - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *scenesource = obs_scene_get_source(scene); - int64_t id = obs_sceneitem_get_id(sceneitem); - const char *name = obs_source_get_name(scenesource); - const char *uuid = obs_source_get_uuid(scenesource); - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - - auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { - OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); - obs_scene_t *sc = obs_group_or_scene_from_source(s); - obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); - if (si) - obs_sceneitem_set_visible(si, val); - }; - - QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); - - OBSBasic *main = OBSBasic::Get(); - main->undo_s.add_action(str.arg(obs_source_get_name(source), name), - std::bind(undo_redo, std::placeholders::_1, id, !val), - std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); - - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_visible(sceneitem, val); - }; - - auto setItemLocked = [this](bool checked) { - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_locked(sceneitem, checked); - }; - - connect(vis, &QAbstractButton::clicked, setItemVisible); - connect(lock, &QAbstractButton::clicked, setItemLocked); -} - -void SourceTreeItem::paintEvent(QPaintEvent *event) -{ - QStyleOption opt; - opt.initFrom(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - QWidget::paintEvent(event); -} - -void SourceTreeItem::DisconnectSignals() -{ - sigs.clear(); -} - -void SourceTreeItem::Clear() -{ - DisconnectSignals(); - sceneitem = nullptr; -} - -void SourceTreeItem::ReconnectSignals() -{ - if (!sceneitem) - return; - - DisconnectSignals(); - - /* --------------------------------------------------------- */ - - auto removeItem = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); - - if (curItem == this_->sceneitem) { - QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), - Q_ARG(OBSScene, curScene)); - curItem = nullptr; - } - if (!curItem) - QMetaObject::invokeMethod(this_, "Clear"); - }; - - auto itemVisible = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool visible = calldata_bool(cd, "visible"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); - }; - - auto itemLocked = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool locked = calldata_bool(cd, "locked"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); - }; - - auto itemSelect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Select"); - }; - - auto itemDeselect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Deselect"); - }; - - auto reorderGroup = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - QMetaObject::invokeMethod(this_->tree, "ReorderItems"); - }; - - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *sceneSource = obs_scene_get_source(scene); - signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); - - sigs.emplace_back(signal, "remove", removeItem, this); - sigs.emplace_back(signal, "item_remove", removeItem, this); - sigs.emplace_back(signal, "item_visible", itemVisible, this); - sigs.emplace_back(signal, "item_locked", itemLocked, this); - sigs.emplace_back(signal, "item_select", itemSelect, this); - sigs.emplace_back(signal, "item_deselect", itemDeselect, this); - - if (obs_sceneitem_is_group(sceneitem)) { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - - sigs.emplace_back(signal, "reorder", reorderGroup, this); - } - - /* --------------------------------------------------------- */ - - auto removeSource = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - this_->DisconnectSignals(); - this_->sceneitem = nullptr; - QMetaObject::invokeMethod(this_->tree, "RefreshItems"); - }; - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - sigs.emplace_back(signal, "remove", removeSource, this); -} - -void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); - - if (expand) { - expand->setChecked(!expand->isChecked()); - } else { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - if (obs_source_configurable(source)) { - main->CreatePropertiesWindow(source); - } - } -} - -void SourceTreeItem::enterEvent(QEnterEvent *event) -{ - QWidget::enterEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); - preview->hoveredPreviewItems.push_back(sceneitem); -} - -void SourceTreeItem::leaveEvent(QEvent *event) -{ - QWidget::leaveEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); -} - -bool SourceTreeItem::IsEditing() -{ - return editor != nullptr; -} - -void SourceTreeItem::EnterEditMode() -{ - setFocusPolicy(Qt::StrongFocus); - int index = boxLayout->indexOf(label); - boxLayout->removeWidget(label); - editor = new QLineEdit(label->text()); - editor->setStyleSheet("background: none"); - editor->selectAll(); - editor->installEventFilter(this); - boxLayout->insertWidget(index, editor); - setFocusProxy(editor); -} - -void SourceTreeItem::ExitEditMode(bool save) -{ - ExitEditModeInternal(save); - - if (tree->undoSceneData) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); - main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); - - tree->undoSceneData = nullptr; - } -} - -void SourceTreeItem::ExitEditModeInternal(bool save) -{ - if (!editor) { - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSScene scene = main->GetCurrentScene(); - - newName = QT_TO_UTF8(editor->text()); - - setFocusProxy(nullptr); - int index = boxLayout->indexOf(editor); - boxLayout->removeWidget(editor); - delete editor; - editor = nullptr; - setFocusPolicy(Qt::NoFocus); - boxLayout->insertWidget(index, label); - setFocus(); - - /* ----------------------------------------- */ - /* check for empty string */ - - if (!save) - return; - - if (newName.empty()) { - OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; - } - - /* ----------------------------------------- */ - /* Check for same name */ - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - if (newName == obs_source_get_name(source)) - return; - - /* ----------------------------------------- */ - /* check for existing source */ - - OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); - bool exists = !!existingSource; - - if (exists) { - OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - return; - } - - /* ----------------------------------------- */ - /* rename */ - - QSignalBlocker sourcesSignalBlocker(this); - std::string prevName(obs_source_get_name(source)); - std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); - auto undo = [scene_uuid, prevName, main](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prevName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - std::string editedName = newName; - - auto redo = [scene_uuid, main, editedName](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, editedName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - const char *uuid = obs_source_get_uuid(source); - main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); - - obs_source_set_name(source, newName.c_str()); -} - -bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) -{ - if (editor != object) - return false; - - if (LineEditCanceled(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); - return true; - } - if (LineEditChanged(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); - return true; - } - - return false; -} - -void SourceTreeItem::VisibilityChanged(bool visible) -{ - if (iconLabel) { - iconLabel->setEnabled(visible); - } - label->setEnabled(visible); - vis->setChecked(visible); -} - -void SourceTreeItem::LockedChanged(bool locked) -{ - lock->setChecked(locked); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Update(bool force) -{ - OBSScene scene = GetCurrentScene(); - obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); - - Type newType; - - /* ------------------------------------------------- */ - /* if it's a group item, insert group checkbox */ - - if (obs_sceneitem_is_group(sceneitem)) { - newType = Type::Group; - - /* ------------------------------------------------- */ - /* if it's a group sub-item */ - - } else if (itemScene != scene) { - newType = Type::SubItem; - - /* ------------------------------------------------- */ - /* if it's a regular item */ - - } else { - newType = Type::Item; - } - - /* ------------------------------------------------- */ - - if (!force && newType == type) { - return; - } - - /* ------------------------------------------------- */ - - ReconnectSignals(); - - if (spacer) { - boxLayout->removeItem(spacer); - delete spacer; - spacer = nullptr; - } - - if (type == Type::Group) { - boxLayout->removeWidget(expand); - expand->deleteLater(); - expand = nullptr; - } - - type = newType; - - if (type == Type::SubItem) { - spacer = new QSpacerItem(16, 1); - boxLayout->insertItem(0, spacer); - - } else if (type == Type::Group) { - expand = new QCheckBox(); - expand->setProperty("class", "checkbox-icon indicator-expand"); -#ifdef __APPLE__ - expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - boxLayout->insertWidget(0, expand); - - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - expand->blockSignals(true); - expand->setChecked(obs_data_get_bool(data, "collapsed")); - expand->blockSignals(false); - - connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); - - } else { - spacer = new QSpacerItem(3, 1); - boxLayout->insertItem(0, spacer); - } -} - -void SourceTreeItem::ExpandClicked(bool checked) -{ - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - - obs_data_set_bool(data, "collapsed", checked); - - if (!checked) - tree->GetStm()->ExpandGroup(sceneitem); - else - tree->GetStm()->CollapseGroup(sceneitem); -} - -void SourceTreeItem::Select() -{ - tree->SelectItem(sceneitem, true); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Deselect() -{ - tree->SelectItem(sceneitem, false); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -/* ========================================================================= */ - void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) { SourceTreeModel *stm = reinterpret_cast(ptr); @@ -978,636 +432,3 @@ void SourceTreeModel::UpdateGroupState(bool update) } } } - -/* ========================================================================= */ - -SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) -{ - SourceTreeModel *stm_ = new SourceTreeModel(this); - setModel(stm_); - setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - UpdateNoSourcesMessage(); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); - - setItemDelegate(new SourceTreeDelegate(this)); -} - -void SourceTree::UpdateIcons() -{ - SourceTreeModel *stm = GetStm(); - stm->SceneChanged(); -} - -void SourceTree::SetIconsVisible(bool visible) -{ - SourceTreeModel *stm = GetStm(); - - iconsVisible = visible; - stm->SceneChanged(); -} - -void SourceTree::ResetWidgets() -{ - OBSScene scene = GetCurrentScene(); - - SourceTreeModel *stm = GetStm(); - stm->UpdateGroupState(false); - - for (int i = 0; i < stm->items.count(); i++) { - QModelIndex index = stm->createIndex(i, 0, nullptr); - setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); - } -} - -void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) -{ - setIndexWidget(idx, new SourceTreeItem(this, item)); -} - -void SourceTree::UpdateWidgets(bool force) -{ - SourceTreeModel *stm = GetStm(); - - for (int i = 0; i < stm->items.size(); i++) { - obs_sceneitem_t *item = stm->items[i]; - SourceTreeItem *widget = GetItemWidget(i); - - if (!widget) { - UpdateWidget(stm->createIndex(i, 0), item); - } else { - widget->Update(force); - } - } -} - -void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) -{ - SourceTreeModel *stm = GetStm(); - int i = 0; - - for (; i < stm->items.count(); i++) { - if (stm->items[i] == sceneitem) - break; - } - - if (i == stm->items.count()) - return; - - QModelIndex index = stm->createIndex(i, 0); - if (index.isValid() && select != selectionModel()->isSelected(index)) - selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); -} - -Q_DECLARE_METATYPE(OBSSceneItem); - -void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - QListView::mouseDoubleClickEvent(event); -} - -void SourceTree::dropEvent(QDropEvent *event) -{ - if (event->source() != this) { - QListView::dropEvent(event); - return; - } - - OBSBasic *main = OBSBasic::Get(); - - OBSScene scene = GetCurrentScene(); - obs_source_t *scenesource = obs_scene_get_source(scene); - SourceTreeModel *stm = GetStm(); - auto &items = stm->items; - QModelIndexList indices = selectedIndexes(); - - DropIndicatorPosition indicator = dropIndicatorPosition(); - int row = indexAt(event->position().toPoint()).row(); - bool emptyDrop = row == -1; - - if (emptyDrop) { - if (!items.size()) { - QListView::dropEvent(event); - return; - } - - row = items.size() - 1; - indicator = QAbstractItemView::BelowItem; - } - - /* --------------------------------------- */ - /* store destination group if moving to a */ - /* group */ - - obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ - bool itemIsGroup = obs_sceneitem_is_group(dropItem); - - obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); - - /* not a group if moving above the group */ - if (indicator == QAbstractItemView::AboveItem && itemIsGroup) - dropGroup = nullptr; - if (emptyDrop) - dropGroup = nullptr; - - /* --------------------------------------- */ - /* remember to remove list items if */ - /* dropping on collapsed group */ - - bool dropOnCollapsed = false; - if (dropGroup) { - obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); - dropOnCollapsed = obs_data_get_bool(data, "collapsed"); - obs_data_release(data); - } - - if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || - indicator == QAbstractItemView::OnViewport) - row++; - - if (row < 0 || row > stm->items.count()) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* determine if any base group is selected */ - - bool hasGroups = false; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_is_group(item)) { - hasGroups = true; - break; - } - } - - /* --------------------------------------- */ - /* if dropping a group, detect if it's */ - /* below another group */ - - obs_sceneitem_t *itemBelow; - if (row == stm->items.count()) - itemBelow = nullptr; - else - itemBelow = stm->items[row]; - - if (hasGroups) { - if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { - dropGroup = nullptr; - dropOnCollapsed = false; - } - } - - /* --------------------------------------- */ - /* if dropping groups on other groups, */ - /* disregard as invalid drag/drop */ - - if (dropGroup && hasGroups) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* save undo data */ - std::vector sources; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_get_scene(item) != scene) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - } - if (dropGroup) - sources.push_back(obs_sceneitem_get_source(dropGroup)); - OBSData undo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* if selection includes base group items, */ - /* include all group sub-items and treat */ - /* them all as one */ - - if (hasGroups) { - /* remove sub-items if selected */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_scene_t *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - indices.removeAt(i); - } - } - - /* add all sub-items of selected groups */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - - if (obs_sceneitem_is_group(item)) { - for (int j = items.size() - 1; j >= 0; j--) { - obs_sceneitem_t *subitem = items[j]; - obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); - - if (subitemGroup == item) { - QModelIndex idx = stm->createIndex(j, 0); - indices.insert(i + 1, idx); - } - } - } - } - } - - /* --------------------------------------- */ - /* build persistent indices */ - - QList persistentIndices; - persistentIndices.reserve(indices.count()); - for (QModelIndex &index : indices) - persistentIndices.append(index); - std::sort(persistentIndices.begin(), persistentIndices.end()); - - /* --------------------------------------- */ - /* move all items to destination index */ - - int r = row; - for (auto &persistentIdx : persistentIndices) { - int from = persistentIdx.row(); - int to = r; - int itemTo = to; - - if (itemTo > from) - itemTo--; - - if (itemTo != from) { - stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); - MoveItem(items, from, itemTo); - stm->endMoveRows(); - } - - r = persistentIdx.row() + 1; - } - - std::sort(persistentIndices.begin(), persistentIndices.end()); - int firstIdx = persistentIndices.front().row(); - int lastIdx = persistentIndices.back().row(); - - /* --------------------------------------- */ - /* reorder scene items in back-end */ - - QVector orderList; - obs_sceneitem_t *lastGroup = nullptr; - int insertCollapsedIdx = 0; - - auto insertCollapsed = [&](obs_sceneitem_t *item) { - struct obs_sceneitem_order_info info; - info.group = lastGroup; - info.item = item; - - orderList.insert(insertCollapsedIdx++, info); - }; - - using insertCollapsed_t = decltype(insertCollapsed); - - auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - (*reinterpret_cast(param))(item); - return true; - }; - - auto insertLastGroup = [&]() { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); - bool collapsed = obs_data_get_bool(data, "collapsed"); - - if (collapsed) { - insertCollapsedIdx = 0; - obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); - } - - struct obs_sceneitem_order_info info; - info.group = nullptr; - info.item = lastGroup; - orderList.insert(0, info); - }; - - auto updateScene = [&]() { - struct obs_sceneitem_order_info info; - - for (int i = 0; i < items.size(); i++) { - obs_sceneitem_t *item = items[i]; - obs_sceneitem_t *group; - - if (obs_sceneitem_is_group(item)) { - if (lastGroup) { - insertLastGroup(); - } - lastGroup = item; - continue; - } - - if (!hasGroups && i >= firstIdx && i <= lastIdx) - group = dropGroup; - else - group = obs_sceneitem_get_group(scene, item); - - if (lastGroup && lastGroup != group) { - insertLastGroup(); - } - - lastGroup = group; - - info.group = group; - info.item = item; - orderList.insert(0, info); - } - - if (lastGroup) { - insertLastGroup(); - } - - obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); - }; - - using updateScene_t = decltype(updateScene); - - auto preUpdateScene = [](void *data, obs_scene_t *) { - (*reinterpret_cast(data))(); - }; - - ignoreReorder = true; - obs_scene_atomic_update(scene, preUpdateScene, &updateScene); - ignoreReorder = false; - - /* --------------------------------------- */ - /* save redo data */ - - OBSData redo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* add undo/redo action */ - - const char *scene_name = obs_source_get_name(scenesource); - QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); - main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); - - /* --------------------------------------- */ - /* remove items if dropped in to collapsed */ - /* group */ - - if (dropOnCollapsed) { - stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); - items.remove(firstIdx, lastIdx - firstIdx + 1); - stm->endRemoveRows(); - } - - /* --------------------------------------- */ - /* update widgets and accept event */ - - UpdateWidgets(true); - - event->accept(); - event->setDropAction(Qt::CopyAction); - - QListView::dropEvent(event); -} - -void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) -{ - { - QSignalBlocker sourcesSignalBlocker(this); - SourceTreeModel *stm = GetStm(); - - QModelIndexList selectedIdxs = selected.indexes(); - QModelIndexList deselectedIdxs = deselected.indexes(); - - for (int i = 0; i < selectedIdxs.count(); i++) { - int idx = selectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], true); - } - - for (int i = 0; i < deselectedIdxs.count(); i++) { - int idx = deselectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], false); - } - } - QListView::selectionChanged(selected, deselected); -} - -void SourceTree::NewGroupEdit(int row) -{ - if (!Edit(row)) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " - "code should never be reached.\nAnd by " - "\"never be reached\", I mean that " - "theoretically, it should be\nimpossible " - "for this code to be reached. But if this " - "code is reached,\nfeel free to laugh at " - "Lain, because apparently it is, in fact, " - "actually\npossible for this code to be " - "reached. But I mean, again, theoretically\n" - "it should be impossible. So if you see " - "this in your log, just know that\nit's " - "really dumb, and depressing. But at least " - "the undo/redo action is\nstill covered, so " - "in theory things *should* be fine. But " - "it's entirely\npossible that they might " - "not be exactly. But again, yea. This " - "really\nshould not be possible."); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg("Unknown"); - main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); - - undoSceneData = nullptr; - } -} - -bool SourceTree::Edit(int row) -{ - SourceTreeModel *stm = GetStm(); - if (row < 0 || row >= stm->items.count()) - return false; - - QModelIndex index = stm->createIndex(row, 0); - QWidget *widget = indexWidget(index); - SourceTreeItem *itemWidget = reinterpret_cast(widget); - if (itemWidget->IsEditing()) { -#ifdef __APPLE__ - itemWidget->ExitEditMode(true); -#endif - return false; - } - - itemWidget->EnterEditMode(); - edit(index); - return true; -} - -bool SourceTree::MultipleBaseSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (obs_sceneitem_is_group(item)) { - return false; - } - - obs_scene *itemScene = obs_sceneitem_get_scene(item); - if (itemScene != scene) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (!obs_sceneitem_is_group(item)) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupedItemsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - OBSScene scene = GetCurrentScene(); - - if (!selectedIndices.size()) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - obs_scene *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - return true; - } - } - - return false; -} - -void SourceTree::Remove(OBSSceneItem item, OBSScene scene) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - GetStm()->Remove(item); - main->SaveProject(); - - if (!main->SavingDisabled()) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - } -} - -void SourceTree::GroupSelectedItems() -{ - QModelIndexList indices = selectedIndexes(); - std::sort(indices.begin(), indices.end()); - GetStm()->GroupSelectedItems(indices); -} - -void SourceTree::UngroupSelectedGroups() -{ - QModelIndexList indices = selectedIndexes(); - GetStm()->UngroupSelectedGroups(indices); -} - -void SourceTree::AddGroup() -{ - GetStm()->AddGroup(); -} - -void SourceTree::UpdateNoSourcesMessage() -{ - QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; - iconNoSources.load(file); - - QTextOption opt(Qt::AlignHCenter); - opt.setWrapMode(QTextOption::WordWrap); - textNoSources.setTextOption(opt); - textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); - - textPrepared = false; -} - -void SourceTree::paintEvent(QPaintEvent *event) -{ - SourceTreeModel *stm = GetStm(); - if (stm && !stm->items.count()) { - QPainter p(viewport()); - - if (!textPrepared) { - textNoSources.prepare(QTransform(), p.font()); - textPrepared = true; - } - - QRectF iconRect = iconNoSources.viewBoxF(); - iconRect.setSize(QSizeF(32.0, 32.0)); - - QSizeF iconSize = iconRect.size(); - QSizeF textSize = textNoSources.size(); - QSizeF thisSize = size(); - const qreal spacing = 16.0; - - qreal totalHeight = iconSize.height() + spacing + textSize.height(); - - qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; - qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; - iconRect.moveTo(std::round(x), std::round(y)); - iconNoSources.render(&p, iconRect); - - x = thisSize.width() / 2.0 - textSize.width() / 2.0; - y += spacing + iconSize.height(); - p.drawStaticText(x, y, textNoSources); - } else { - QListView::paintEvent(event); - } -} - -SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - SourceTree *tree = qobject_cast(parent()); - QWidget *item = tree->indexWidget(index); - - if (!item) - return (QSize(0, 0)); - - return (QSize(option.widget->minimumWidth(), item->height())); -} diff --git a/frontend/components/SourceTreeModel.hpp b/frontend/components/SourceTreeModel.hpp index 8f8922d52..a94835346 100644 --- a/frontend/components/SourceTreeModel.hpp +++ b/frontend/components/SourceTreeModel.hpp @@ -1,91 +1,11 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include +#include + +#include -class QLabel; -class OBSSourceLabel; -class QCheckBox; -class QLineEdit; class SourceTree; -class QSpacerItem; -class QHBoxLayout; -class VisibilityItemWidget; - -class SourceTreeItem : public QFrame { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeModel; - - void mouseDoubleClickEvent(QMouseEvent *event) override; - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual bool eventFilter(QObject *object, QEvent *event) override; - - void Update(bool force); - - enum class Type { - Unknown, - Item, - Group, - SubItem, - }; - - void DisconnectSignals(); - void ReconnectSignals(); - - Type type = Type::Unknown; - -public: - explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); - bool IsEditing(); - -private: - QSpacerItem *spacer = nullptr; - QCheckBox *expand = nullptr; - QLabel *iconLabel = nullptr; - QCheckBox *vis = nullptr; - QCheckBox *lock = nullptr; - QHBoxLayout *boxLayout = nullptr; - OBSSourceLabel *label = nullptr; - - QLineEdit *editor = nullptr; - - std::string newName; - - SourceTree *tree; - OBSSceneItem sceneitem; - std::vector sigs; - - virtual void paintEvent(QPaintEvent *event) override; - - void ExitEditModeInternal(bool save); - -private slots: - void Clear(); - - void EnterEditMode(); - void ExitEditMode(bool save); - - void VisibilityChanged(bool visible); - void LockedChanged(bool locked); - - void ExpandClicked(bool checked); - - void Select(); - void Deselect(); -}; class SourceTreeModel : public QAbstractListModel { Q_OBJECT @@ -125,78 +45,3 @@ public: virtual Qt::ItemFlags flags(const QModelIndex &index) const override; virtual Qt::DropActions supportedDropActions() const override; }; - -class SourceTree : public QListView { - Q_OBJECT - - bool ignoreReorder = false; - - friend class SourceTreeModel; - friend class SourceTreeItem; - - bool textPrepared = false; - QStaticText textNoSources; - QSvgRenderer iconNoSources; - - OBSData undoSceneData; - - bool iconsVisible = true; - - void UpdateNoSourcesMessage(); - - void ResetWidgets(); - void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); - void UpdateWidgets(bool force = false); - - inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } - -public: - inline SourceTreeItem *GetItemWidget(int idx) - { - QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); - return reinterpret_cast(widget); - } - - explicit SourceTree(QWidget *parent = nullptr); - - inline bool IgnoreReorder() const { return ignoreReorder; } - inline void Clear() { GetStm()->Clear(); } - - inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } - inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } - inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } - - void SelectItem(obs_sceneitem_t *sceneitem, bool select); - - bool MultipleBaseSelected() const; - bool GroupsSelected() const; - bool GroupedItemsSelected() const; - - void UpdateIcons(); - void SetIconsVisible(bool visible); - -public slots: - inline void ReorderItems() { GetStm()->ReorderItems(); } - inline void RefreshItems() { GetStm()->SceneChanged(); } - void Remove(OBSSceneItem item, OBSScene scene); - void GroupSelectedItems(); - void UngroupSelectedGroups(); - void AddGroup(); - bool Edit(int idx); - void NewGroupEdit(int idx); - -protected: - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void dropEvent(QDropEvent *event) override; - virtual void paintEvent(QPaintEvent *event) override; - - virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; -}; - -class SourceTreeDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SourceTreeDelegate(QObject *parent); - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; -}; diff --git a/frontend/components/TextSourceToolbar.cpp b/frontend/components/TextSourceToolbar.cpp index c45d06976..be2d46525 100644 --- a/frontend/components/TextSourceToolbar.cpp +++ b/frontend/components/TextSourceToolbar.cpp @@ -1,574 +1,18 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" +#include "TextSourceToolbar.hpp" +#include "ui_text-source-toolbar.h" + +#include #include -#include + #include #include -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ +#include "moc_TextSourceToolbar.cpp" extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); +extern QColor color_from_int(long long val); +extern long long color_to_int(QColor color); TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) : SourceToolbar(parent, source), diff --git a/frontend/components/TextSourceToolbar.hpp b/frontend/components/TextSourceToolbar.hpp index acf88a5fb..38833803a 100644 --- a/frontend/components/TextSourceToolbar.hpp +++ b/frontend/components/TextSourceToolbar.hpp @@ -1,165 +1,9 @@ #pragma once -#include -#include -#include +#include "SourceToolbar.hpp" -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; class Ui_TextSourceToolbar; -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - class TextSourceToolbar : public SourceToolbar { Q_OBJECT diff --git a/frontend/components/VisibilityItemDelegate.cpp b/frontend/components/VisibilityItemDelegate.cpp index 50ea425ad..d9e76ee5c 100644 --- a/frontend/components/VisibilityItemDelegate.cpp +++ b/frontend/components/VisibilityItemDelegate.cpp @@ -1,69 +1,9 @@ -#include "moc_visibility-item-widget.cpp" -#include "obs-app.hpp" -#include "source-label.hpp" +#include "VisibilityItemWidget.hpp" +#include "VisibilityItemDelegate.hpp" -#include -#include -#include -#include -#include -#include #include -#include -VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_) - : source(source_), - enabledSignal(obs_source_get_signal_handler(source), "enable", OBSSourceEnabled, this) -{ - bool enabled = obs_source_enabled(source); - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - vis->setChecked(enabled); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - QHBoxLayout *itemLayout = new QHBoxLayout(); - itemLayout->addWidget(vis); - itemLayout->addWidget(label); - itemLayout->setContentsMargins(0, 0, 0, 0); - - setLayout(itemLayout); - - connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); }); -} - -void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data) -{ - VisibilityItemWidget *window = reinterpret_cast(param); - bool enabled = calldata_bool(data, "enabled"); - - QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled)); -} - -void VisibilityItemWidget::SourceEnabled(bool enabled) -{ - if (vis->isChecked() != enabled) - vis->setChecked(enabled); -} - -void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_) -{ - /* Do not update unless the state has actually changed */ - if (active_ == active && selected_ == selected) - return; - - QPalette pal = vis->palette(); - pal.setColor(QPalette::WindowText, color); - vis->setPalette(pal); - - label->setStyleSheet(QString("color: %1;").arg(color.name())); - - active = active_; - selected = selected_; -} +#include "moc_VisibilityItemDelegate.cpp" VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} @@ -124,10 +64,3 @@ bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event) return QStyledItemDelegate::eventFilter(object, event); } - -void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source) -{ - VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source); - - list->setItemWidget(item, baseWidget); -} diff --git a/frontend/components/VisibilityItemDelegate.hpp b/frontend/components/VisibilityItemDelegate.hpp index ec6b35e37..d81b51dae 100644 --- a/frontend/components/VisibilityItemDelegate.hpp +++ b/frontend/components/VisibilityItemDelegate.hpp @@ -1,39 +1,8 @@ #pragma once -#include #include -#include -class QLabel; -class QLineEdit; -class QListWidget; -class QListWidgetItem; -class QCheckBox; -class OBSSourceLabel; - -class VisibilityItemWidget : public QWidget { - Q_OBJECT - -private: - OBSSource source; - OBSSourceLabel *label = nullptr; - QCheckBox *vis = nullptr; - - OBSSignal enabledSignal; - - bool active = false; - bool selected = false; - - static void OBSSourceEnabled(void *param, calldata_t *data); - -private slots: - void SourceEnabled(bool enabled); - -public: - VisibilityItemWidget(obs_source_t *source); - - void SetColor(const QColor &color, bool active, bool selected); -}; +class QObject; class VisibilityItemDelegate : public QStyledItemDelegate { Q_OBJECT @@ -46,5 +15,3 @@ public: protected: bool eventFilter(QObject *object, QEvent *event) override; }; - -void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source); diff --git a/frontend/components/VisibilityItemWidget.cpp b/frontend/components/VisibilityItemWidget.cpp index 50ea425ad..1283f6fbe 100644 --- a/frontend/components/VisibilityItemWidget.cpp +++ b/frontend/components/VisibilityItemWidget.cpp @@ -1,15 +1,11 @@ -#include "moc_visibility-item-widget.cpp" -#include "obs-app.hpp" -#include "source-label.hpp" +#include "VisibilityItemWidget.hpp" + +#include -#include -#include -#include -#include -#include -#include -#include #include +#include + +#include "moc_VisibilityItemWidget.cpp" VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_) : source(source_), @@ -65,66 +61,6 @@ void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool sele selected = selected_; } -VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyledItemDelegate::paint(painter, option, index); - - QObject *parentObj = parent(); - QListWidget *list = qobject_cast(parentObj); - if (!list) - return; - - QListWidgetItem *item = list->item(index.row()); - VisibilityItemWidget *widget = qobject_cast(list->itemWidget(item)); - if (!widget) - return; - - bool selected = option.state.testFlag(QStyle::State_Selected); - bool active = option.state.testFlag(QStyle::State_Active); - - QPalette palette = list->palette(); -#if defined(_WIN32) || defined(__APPLE__) - QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Inactive; -#else - QPalette::ColorGroup group = QPalette::Active; -#endif - -#ifdef _WIN32 - QPalette::ColorRole highlightRole = QPalette::WindowText; -#else - QPalette::ColorRole highlightRole = QPalette::HighlightedText; -#endif - - QPalette::ColorRole role; - - if (selected && active) - role = highlightRole; - else - role = QPalette::WindowText; - - widget->SetColor(palette.color(group, role), active, selected); -} - -bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event) -{ - QWidget *editor = qobject_cast(object); - if (!editor) - return false; - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - - if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) { - return false; - } - } - - return QStyledItemDelegate::eventFilter(object, event); -} - void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source) { VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source); diff --git a/frontend/components/VisibilityItemWidget.hpp b/frontend/components/VisibilityItemWidget.hpp index ec6b35e37..3c4322c87 100644 --- a/frontend/components/VisibilityItemWidget.hpp +++ b/frontend/components/VisibilityItemWidget.hpp @@ -1,15 +1,12 @@ #pragma once -#include -#include #include -class QLabel; -class QLineEdit; -class QListWidget; -class QListWidgetItem; -class QCheckBox; +#include +#include + class OBSSourceLabel; +class QCheckBox; class VisibilityItemWidget : public QWidget { Q_OBJECT @@ -35,16 +32,4 @@ public: void SetColor(const QColor &color, bool active, bool selected); }; -class VisibilityItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - VisibilityItemDelegate(QObject *parent = nullptr); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -protected: - bool eventFilter(QObject *object, QEvent *event) override; -}; - void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source); diff --git a/frontend/components/WindowCaptureToolbar.cpp b/frontend/components/WindowCaptureToolbar.cpp index c45d06976..3f1e4ebba 100644 --- a/frontend/components/WindowCaptureToolbar.cpp +++ b/frontend/components/WindowCaptureToolbar.cpp @@ -1,18 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" +#include "WindowCaptureToolbar.hpp" #include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" +#include "moc_WindowCaptureToolbar.cpp" #ifdef _WIN32 #define get_os_module(win, mac, linux) obs_get_module(win) @@ -25,205 +13,6 @@ #define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) #endif -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} void WindowCaptureToolbar::Init() @@ -250,458 +39,3 @@ void WindowCaptureToolbar::Init() ComboSelectToolbar::Init(); } - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/frontend/components/WindowCaptureToolbar.hpp b/frontend/components/WindowCaptureToolbar.hpp index acf88a5fb..760a95402 100644 --- a/frontend/components/WindowCaptureToolbar.hpp +++ b/frontend/components/WindowCaptureToolbar.hpp @@ -1,77 +1,6 @@ #pragma once -#include -#include -#include - -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; +#include "ComboSelectToolbar.hpp" class WindowCaptureToolbar : public ComboSelectToolbar { Q_OBJECT @@ -80,99 +9,3 @@ public: WindowCaptureToolbar(QWidget *parent, OBSSource source); void Init() override; }; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; From e4a43f655506a6873199d165199d368e6fcf437b Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Thu, 21 Nov 2024 20:16:07 +0100 Subject: [PATCH 05/37] frontend: Prepare Qt UI dialogs for splits --- .../components/DelButton.hpp | 0 frontend/components/EditWidget.hpp | 562 +++++++++++ .../dialogs/OBSBasicInteraction.cpp | 0 .../dialogs/OBSBasicInteraction.hpp | 0 frontend/dialogs/OBSExtraBrowsers.cpp | 562 +++++++++++ .../dialogs/OBSExtraBrowsers.hpp | 0 .../dialogs/OBSMissingFiles.cpp | 0 .../dialogs/OBSMissingFiles.hpp | 0 .../dialogs/OBSRemux.cpp | 0 .../dialogs/OBSRemux.hpp | 0 frontend/utility/ExtraBrowsersDelegate.cpp | 562 +++++++++++ frontend/utility/ExtraBrowsersDelegate.hpp | 88 ++ frontend/utility/ExtraBrowsersModel.cpp | 562 +++++++++++ frontend/utility/ExtraBrowsersModel.hpp | 88 ++ frontend/utility/MissingFilesModel.cpp | 542 ++++++++++ frontend/utility/MissingFilesModel.hpp | 112 +++ .../utility/MissingFilesPathItemDelegate.cpp | 542 ++++++++++ .../utility/MissingFilesPathItemDelegate.hpp | 112 +++ frontend/utility/OBSEventFilter.hpp | 82 ++ .../utility/RemuxEntryPathItemDelegate.cpp | 924 ++++++++++++++++++ .../utility/RemuxEntryPathItemDelegate.hpp | 173 ++++ frontend/utility/RemuxQueueModel.cpp | 924 ++++++++++++++++++ frontend/utility/RemuxQueueModel.hpp | 173 ++++ frontend/utility/RemuxWorker.cpp | 924 ++++++++++++++++++ frontend/utility/RemuxWorker.hpp | 173 ++++ 25 files changed, 7105 insertions(+) rename UI/window-extra-browsers.cpp => frontend/components/DelButton.hpp (100%) create mode 100644 frontend/components/EditWidget.hpp rename UI/window-basic-interaction.cpp => frontend/dialogs/OBSBasicInteraction.cpp (100%) rename UI/window-basic-interaction.hpp => frontend/dialogs/OBSBasicInteraction.hpp (100%) create mode 100644 frontend/dialogs/OBSExtraBrowsers.cpp rename UI/window-extra-browsers.hpp => frontend/dialogs/OBSExtraBrowsers.hpp (100%) rename UI/window-missing-files.cpp => frontend/dialogs/OBSMissingFiles.cpp (100%) rename UI/window-missing-files.hpp => frontend/dialogs/OBSMissingFiles.hpp (100%) rename UI/window-remux.cpp => frontend/dialogs/OBSRemux.cpp (100%) rename UI/window-remux.hpp => frontend/dialogs/OBSRemux.hpp (100%) create mode 100644 frontend/utility/ExtraBrowsersDelegate.cpp create mode 100644 frontend/utility/ExtraBrowsersDelegate.hpp create mode 100644 frontend/utility/ExtraBrowsersModel.cpp create mode 100644 frontend/utility/ExtraBrowsersModel.hpp create mode 100644 frontend/utility/MissingFilesModel.cpp create mode 100644 frontend/utility/MissingFilesModel.hpp create mode 100644 frontend/utility/MissingFilesPathItemDelegate.cpp create mode 100644 frontend/utility/MissingFilesPathItemDelegate.hpp create mode 100644 frontend/utility/OBSEventFilter.hpp create mode 100644 frontend/utility/RemuxEntryPathItemDelegate.cpp create mode 100644 frontend/utility/RemuxEntryPathItemDelegate.hpp create mode 100644 frontend/utility/RemuxQueueModel.cpp create mode 100644 frontend/utility/RemuxQueueModel.hpp create mode 100644 frontend/utility/RemuxWorker.cpp create mode 100644 frontend/utility/RemuxWorker.hpp diff --git a/UI/window-extra-browsers.cpp b/frontend/components/DelButton.hpp similarity index 100% rename from UI/window-extra-browsers.cpp rename to frontend/components/DelButton.hpp diff --git a/frontend/components/EditWidget.hpp b/frontend/components/EditWidget.hpp new file mode 100644 index 000000000..8f4232e1d --- /dev/null +++ b/frontend/components/EditWidget.hpp @@ -0,0 +1,562 @@ +#include "moc_window-extra-browsers.cpp" +#include "window-dock-browser.hpp" +#include "window-basic-main.hpp" + +#include +#include +#include +#include + +#include + +#include "ui_OBSExtraBrowsers.h" + +using namespace json11; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +/* ------------------------------------------------------------------------- */ + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + Item item; + item.prevIdx = i; + item.title = main->extraBrowserDockNames[i]; + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} + + QPersistentModelIndex index; +}; + +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("class", "icon-trash"); + del->setObjectName("extraPanelDelete"); + del->setMinimumSize(QSize(20, 20)); + connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); + widget->setRowHeight(idx, 20); + widget->setColumnWidth(idx, 20); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + + if (main->extraBrowserDockNames[idx] != item.title) { + main->extraBrowserDockNames[idx] = item.title; + dock->toggleViewAction()->setText(item.title); + dock->setTitle(item.title); + } + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + QString uuid = QUuid::createUuid().toString(); + uuid.replace(QRegularExpression("[{}-]"), ""); + main->AddExtraBrowserDock(item.title, item.url, uuid, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDockNames.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + if (main->extraBrowserDocks.empty()) + main->extraBrowserMenuDocksSeparator.clear(); + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} + +/* ------------------------------------------------------------------------- */ + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} + +/* ------------------------------------------------------------------------- */ + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() {} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} + +/* ------------------------------------------------------------------------- */ + +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockNames.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + if (!array.empty()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + std::string uuid = item["uuid"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QDockWidget *dock = extraBrowserDocks[i].get(); + QString title = extraBrowserDockNames[i]; + QString url = extraBrowserDockTargets[i]; + QString uuid = dock->property("uuid").toString(); + Json::object obj{ + {"title", QT_TO_UTF8(title)}, + {"url", QT_TO_UTF8(url)}, + {"uuid", QT_TO_UTF8(uuid)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + BrowserDock *dock = new BrowserDock(title); + QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); + bId.replace(QRegularExpression("[{}-]"), ""); + dock->setProperty("uuid", bId); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(80, 80); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + /* Add support for Twitch Dashboard panels */ + if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { + QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); + QRegularExpressionMatch match = re.match(url); + QString username = match.captured(1); + if (username.length() > 0) { + std::string script; + script = "Object.defineProperty(document, 'referrer', { get: () => '"; + script += "https://twitch.tv/"; + script += QT_TO_UTF8(username); + script += "/dashboard/live"; + script += "'});"; + browser->setStartupScript(script); + } + } + + AddDockWidget(dock, Qt::RightDockWidgetArea, true); + extraBrowserDocks.push_back(std::shared_ptr(dock)); + extraBrowserDockNames.push_back(title); + extraBrowserDockTargets.push_back(url); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } +} diff --git a/UI/window-basic-interaction.cpp b/frontend/dialogs/OBSBasicInteraction.cpp similarity index 100% rename from UI/window-basic-interaction.cpp rename to frontend/dialogs/OBSBasicInteraction.cpp diff --git a/UI/window-basic-interaction.hpp b/frontend/dialogs/OBSBasicInteraction.hpp similarity index 100% rename from UI/window-basic-interaction.hpp rename to frontend/dialogs/OBSBasicInteraction.hpp diff --git a/frontend/dialogs/OBSExtraBrowsers.cpp b/frontend/dialogs/OBSExtraBrowsers.cpp new file mode 100644 index 000000000..8f4232e1d --- /dev/null +++ b/frontend/dialogs/OBSExtraBrowsers.cpp @@ -0,0 +1,562 @@ +#include "moc_window-extra-browsers.cpp" +#include "window-dock-browser.hpp" +#include "window-basic-main.hpp" + +#include +#include +#include +#include + +#include + +#include "ui_OBSExtraBrowsers.h" + +using namespace json11; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +/* ------------------------------------------------------------------------- */ + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + Item item; + item.prevIdx = i; + item.title = main->extraBrowserDockNames[i]; + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} + + QPersistentModelIndex index; +}; + +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("class", "icon-trash"); + del->setObjectName("extraPanelDelete"); + del->setMinimumSize(QSize(20, 20)); + connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); + widget->setRowHeight(idx, 20); + widget->setColumnWidth(idx, 20); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + + if (main->extraBrowserDockNames[idx] != item.title) { + main->extraBrowserDockNames[idx] = item.title; + dock->toggleViewAction()->setText(item.title); + dock->setTitle(item.title); + } + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + QString uuid = QUuid::createUuid().toString(); + uuid.replace(QRegularExpression("[{}-]"), ""); + main->AddExtraBrowserDock(item.title, item.url, uuid, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDockNames.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + if (main->extraBrowserDocks.empty()) + main->extraBrowserMenuDocksSeparator.clear(); + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} + +/* ------------------------------------------------------------------------- */ + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} + +/* ------------------------------------------------------------------------- */ + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() {} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} + +/* ------------------------------------------------------------------------- */ + +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockNames.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + if (!array.empty()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + std::string uuid = item["uuid"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QDockWidget *dock = extraBrowserDocks[i].get(); + QString title = extraBrowserDockNames[i]; + QString url = extraBrowserDockTargets[i]; + QString uuid = dock->property("uuid").toString(); + Json::object obj{ + {"title", QT_TO_UTF8(title)}, + {"url", QT_TO_UTF8(url)}, + {"uuid", QT_TO_UTF8(uuid)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + BrowserDock *dock = new BrowserDock(title); + QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); + bId.replace(QRegularExpression("[{}-]"), ""); + dock->setProperty("uuid", bId); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(80, 80); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + /* Add support for Twitch Dashboard panels */ + if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { + QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); + QRegularExpressionMatch match = re.match(url); + QString username = match.captured(1); + if (username.length() > 0) { + std::string script; + script = "Object.defineProperty(document, 'referrer', { get: () => '"; + script += "https://twitch.tv/"; + script += QT_TO_UTF8(username); + script += "/dashboard/live"; + script += "'});"; + browser->setStartupScript(script); + } + } + + AddDockWidget(dock, Qt::RightDockWidgetArea, true); + extraBrowserDocks.push_back(std::shared_ptr(dock)); + extraBrowserDockNames.push_back(title); + extraBrowserDockTargets.push_back(url); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } +} diff --git a/UI/window-extra-browsers.hpp b/frontend/dialogs/OBSExtraBrowsers.hpp similarity index 100% rename from UI/window-extra-browsers.hpp rename to frontend/dialogs/OBSExtraBrowsers.hpp diff --git a/UI/window-missing-files.cpp b/frontend/dialogs/OBSMissingFiles.cpp similarity index 100% rename from UI/window-missing-files.cpp rename to frontend/dialogs/OBSMissingFiles.cpp diff --git a/UI/window-missing-files.hpp b/frontend/dialogs/OBSMissingFiles.hpp similarity index 100% rename from UI/window-missing-files.hpp rename to frontend/dialogs/OBSMissingFiles.hpp diff --git a/UI/window-remux.cpp b/frontend/dialogs/OBSRemux.cpp similarity index 100% rename from UI/window-remux.cpp rename to frontend/dialogs/OBSRemux.cpp diff --git a/UI/window-remux.hpp b/frontend/dialogs/OBSRemux.hpp similarity index 100% rename from UI/window-remux.hpp rename to frontend/dialogs/OBSRemux.hpp diff --git a/frontend/utility/ExtraBrowsersDelegate.cpp b/frontend/utility/ExtraBrowsersDelegate.cpp new file mode 100644 index 000000000..8f4232e1d --- /dev/null +++ b/frontend/utility/ExtraBrowsersDelegate.cpp @@ -0,0 +1,562 @@ +#include "moc_window-extra-browsers.cpp" +#include "window-dock-browser.hpp" +#include "window-basic-main.hpp" + +#include +#include +#include +#include + +#include + +#include "ui_OBSExtraBrowsers.h" + +using namespace json11; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +/* ------------------------------------------------------------------------- */ + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + Item item; + item.prevIdx = i; + item.title = main->extraBrowserDockNames[i]; + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} + + QPersistentModelIndex index; +}; + +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("class", "icon-trash"); + del->setObjectName("extraPanelDelete"); + del->setMinimumSize(QSize(20, 20)); + connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); + widget->setRowHeight(idx, 20); + widget->setColumnWidth(idx, 20); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + + if (main->extraBrowserDockNames[idx] != item.title) { + main->extraBrowserDockNames[idx] = item.title; + dock->toggleViewAction()->setText(item.title); + dock->setTitle(item.title); + } + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + QString uuid = QUuid::createUuid().toString(); + uuid.replace(QRegularExpression("[{}-]"), ""); + main->AddExtraBrowserDock(item.title, item.url, uuid, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDockNames.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + if (main->extraBrowserDocks.empty()) + main->extraBrowserMenuDocksSeparator.clear(); + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} + +/* ------------------------------------------------------------------------- */ + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} + +/* ------------------------------------------------------------------------- */ + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() {} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} + +/* ------------------------------------------------------------------------- */ + +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockNames.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + if (!array.empty()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + std::string uuid = item["uuid"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QDockWidget *dock = extraBrowserDocks[i].get(); + QString title = extraBrowserDockNames[i]; + QString url = extraBrowserDockTargets[i]; + QString uuid = dock->property("uuid").toString(); + Json::object obj{ + {"title", QT_TO_UTF8(title)}, + {"url", QT_TO_UTF8(url)}, + {"uuid", QT_TO_UTF8(uuid)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + BrowserDock *dock = new BrowserDock(title); + QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); + bId.replace(QRegularExpression("[{}-]"), ""); + dock->setProperty("uuid", bId); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(80, 80); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + /* Add support for Twitch Dashboard panels */ + if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { + QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); + QRegularExpressionMatch match = re.match(url); + QString username = match.captured(1); + if (username.length() > 0) { + std::string script; + script = "Object.defineProperty(document, 'referrer', { get: () => '"; + script += "https://twitch.tv/"; + script += QT_TO_UTF8(username); + script += "/dashboard/live"; + script += "'});"; + browser->setStartupScript(script); + } + } + + AddDockWidget(dock, Qt::RightDockWidgetArea, true); + extraBrowserDocks.push_back(std::shared_ptr(dock)); + extraBrowserDockNames.push_back(title); + extraBrowserDockTargets.push_back(url); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } +} diff --git a/frontend/utility/ExtraBrowsersDelegate.hpp b/frontend/utility/ExtraBrowsersDelegate.hpp new file mode 100644 index 000000000..690d784dd --- /dev/null +++ b/frontend/utility/ExtraBrowsersDelegate.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Ui_OBSExtraBrowsers; +class ExtraBrowsersModel; + +class QCefWidget; + +class OBSExtraBrowsers : public QDialog { + Q_OBJECT + + std::unique_ptr ui; + ExtraBrowsersModel *model; + +public: + OBSExtraBrowsers(QWidget *parent); + ~OBSExtraBrowsers(); + + void closeEvent(QCloseEvent *event) override; + +public slots: + void on_apply_clicked(); +}; + +class ExtraBrowsersModel : public QAbstractTableModel { + Q_OBJECT + +public: + inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) + { + Reset(); + QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + struct Item { + int prevIdx; + QString title; + QString url; + }; + + void TabSelection(bool forward); + + void AddDeleteButton(int idx); + void Reset(); + void CheckToAdd(); + void UpdateItem(Item &item); + void DeleteItem(); + void Apply(); + + QVector items; + QVector deleted; + + QString newTitle; + QString newURL; + +public slots: + void Init(); +}; + +class ExtraBrowsersDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + + bool eventFilter(QObject *object, QEvent *event) override; + void RevertText(QLineEdit *edit); + bool UpdateText(QLineEdit *edit); + bool ValidName(const QString &text) const; + + ExtraBrowsersModel *model; +}; diff --git a/frontend/utility/ExtraBrowsersModel.cpp b/frontend/utility/ExtraBrowsersModel.cpp new file mode 100644 index 000000000..8f4232e1d --- /dev/null +++ b/frontend/utility/ExtraBrowsersModel.cpp @@ -0,0 +1,562 @@ +#include "moc_window-extra-browsers.cpp" +#include "window-dock-browser.hpp" +#include "window-basic-main.hpp" + +#include +#include +#include +#include + +#include + +#include "ui_OBSExtraBrowsers.h" + +using namespace json11; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +/* ------------------------------------------------------------------------- */ + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + Item item; + item.prevIdx = i; + item.title = main->extraBrowserDockNames[i]; + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} + + QPersistentModelIndex index; +}; + +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("class", "icon-trash"); + del->setObjectName("extraPanelDelete"); + del->setMinimumSize(QSize(20, 20)); + connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); + widget->setRowHeight(idx, 20); + widget->setColumnWidth(idx, 20); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + + if (main->extraBrowserDockNames[idx] != item.title) { + main->extraBrowserDockNames[idx] = item.title; + dock->toggleViewAction()->setText(item.title); + dock->setTitle(item.title); + } + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + QString uuid = QUuid::createUuid().toString(); + uuid.replace(QRegularExpression("[{}-]"), ""); + main->AddExtraBrowserDock(item.title, item.url, uuid, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDockNames.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + if (main->extraBrowserDocks.empty()) + main->extraBrowserMenuDocksSeparator.clear(); + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} + +/* ------------------------------------------------------------------------- */ + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} + +/* ------------------------------------------------------------------------- */ + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() {} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} + +/* ------------------------------------------------------------------------- */ + +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockNames.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + if (!array.empty()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + std::string uuid = item["uuid"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QDockWidget *dock = extraBrowserDocks[i].get(); + QString title = extraBrowserDockNames[i]; + QString url = extraBrowserDockTargets[i]; + QString uuid = dock->property("uuid").toString(); + Json::object obj{ + {"title", QT_TO_UTF8(title)}, + {"url", QT_TO_UTF8(url)}, + {"uuid", QT_TO_UTF8(uuid)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + BrowserDock *dock = new BrowserDock(title); + QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); + bId.replace(QRegularExpression("[{}-]"), ""); + dock->setProperty("uuid", bId); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(80, 80); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + /* Add support for Twitch Dashboard panels */ + if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { + QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); + QRegularExpressionMatch match = re.match(url); + QString username = match.captured(1); + if (username.length() > 0) { + std::string script; + script = "Object.defineProperty(document, 'referrer', { get: () => '"; + script += "https://twitch.tv/"; + script += QT_TO_UTF8(username); + script += "/dashboard/live"; + script += "'});"; + browser->setStartupScript(script); + } + } + + AddDockWidget(dock, Qt::RightDockWidgetArea, true); + extraBrowserDocks.push_back(std::shared_ptr(dock)); + extraBrowserDockNames.push_back(title); + extraBrowserDockTargets.push_back(url); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } +} diff --git a/frontend/utility/ExtraBrowsersModel.hpp b/frontend/utility/ExtraBrowsersModel.hpp new file mode 100644 index 000000000..690d784dd --- /dev/null +++ b/frontend/utility/ExtraBrowsersModel.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Ui_OBSExtraBrowsers; +class ExtraBrowsersModel; + +class QCefWidget; + +class OBSExtraBrowsers : public QDialog { + Q_OBJECT + + std::unique_ptr ui; + ExtraBrowsersModel *model; + +public: + OBSExtraBrowsers(QWidget *parent); + ~OBSExtraBrowsers(); + + void closeEvent(QCloseEvent *event) override; + +public slots: + void on_apply_clicked(); +}; + +class ExtraBrowsersModel : public QAbstractTableModel { + Q_OBJECT + +public: + inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) + { + Reset(); + QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + struct Item { + int prevIdx; + QString title; + QString url; + }; + + void TabSelection(bool forward); + + void AddDeleteButton(int idx); + void Reset(); + void CheckToAdd(); + void UpdateItem(Item &item); + void DeleteItem(); + void Apply(); + + QVector items; + QVector deleted; + + QString newTitle; + QString newURL; + +public slots: + void Init(); +}; + +class ExtraBrowsersDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + + bool eventFilter(QObject *object, QEvent *event) override; + void RevertText(QLineEdit *edit); + bool UpdateText(QLineEdit *edit); + bool ValidName(const QString &text) const; + + ExtraBrowsersModel *model; +}; diff --git a/frontend/utility/MissingFilesModel.cpp b/frontend/utility/MissingFilesModel.cpp new file mode 100644 index 000000000..57e641a74 --- /dev/null +++ b/frontend/utility/MissingFilesModel.cpp @@ -0,0 +1,542 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + 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 "moc_window-missing-files.cpp" +#include "window-basic-main.hpp" + +#include "obs-app.hpp" + +#include +#include +#include + +#include + +enum MissingFilesColumn { + Source, + OriginalPath, + NewPath, + State, + + Count +}; + +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &) const +{ + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in input cells + if (isOutput) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + + return container; +} + +void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + model->setData(index, list); + } else + model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text(), 0); + } +} + +void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) +{ + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = + QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); + +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } + + if (isSet) + emit commitData(container); +} + +void MissingFilesPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); + container->findChild()->clearFocus(); + ((QWidget *)container->parent())->setFocus(); + emit commitData(container); +} + +/** + Model +**/ + +MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) +{ + QStyle *style = QApplication::style(); + + warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); +} + +int MissingFilesModel::rowCount(const QModelIndex &) const +{ + return files.length(); +} + +int MissingFilesModel::columnCount(const QModelIndex &) const +{ + return MissingFilesColumn::Count; +} + +int MissingFilesModel::found() const +{ + int res = 0; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != Missing && files[i].state != Cleared) + res++; + } + + return res; +} + +QVariant MissingFilesModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= files.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + QFileInfo fi(files[index.row()].originalPath); + + switch (index.column()) { + case MissingFilesColumn::Source: + result = files[index.row()].source; + break; + case MissingFilesColumn::OriginalPath: + result = fi.fileName(); + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + case MissingFilesColumn::State: + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + } + break; + } + } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); + + if (source) { + result = main->GetSourceIcon(obs_source_get_id(source)); + } + } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { + QFont font = QFont(); + font.setBold(true); + + result = font; + } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + + default: + break; + } + } else if (role == Qt::ToolTipRole) { + switch (index.column()) { + case MissingFilesColumn::OriginalPath: + result = files[index.row()].originalPath; + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + default: + break; + } + } + + return result; +} + +Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == MissingFilesColumn::OriginalPath) { + flags &= ~Qt::ItemIsEditable; + } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) +{ + loop = false; + QUrl url = QUrl().fromLocalFile(path); + QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); + + bool prompted = skipPrompt; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != MissingFilesState::Missing) + continue; + + QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); + QString filename = origFile.fileName(); + QString testFile = dir + filename; + + if (os_file_exists(testFile.toStdString().c_str())) { + if (!prompted) { + QMessageBox::StandardButton button = + QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), + QTStr("MissingFiles.AutoSearchText")); + + if (button == QMessageBox::No) + break; + + prompted = true; + } + QModelIndex in = index(i, MissingFilesColumn::NewPath); + setData(in, testFile, 0); + } + } + loop = true; +} + +bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == MissingFilesRole::NewPathsToProcessRole) { + QStringList list = value.toStringList(); + + int row = index.row() + 1; + beginInsertRows(QModelIndex(), row, row); + + MissingFileEntry entry; + entry.originalPath = list[0].replace("\\", "/"); + entry.source = list[1]; + + files.insert(row, entry); + row++; + + endInsertRows(); + + success = true; + } else { + QString path = value.toString(); + if (index.column() == MissingFilesColumn::NewPath) { + files[index.row()].newPath = value.toString(); + QString fileName = QUrl(path).fileName(); + QString origFileName = QUrl(files[index.row()].originalPath).fileName(); + + if (path.isEmpty()) { + files[index.row()].state = MissingFilesState::Missing; + } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { + files[index.row()].state = MissingFilesState::Cleared; + } else if (fileName.compare(origFileName) == 0) { + files[index.row()].state = MissingFilesState::Found; + + if (loop) + fileCheckLoop(files, path, false); + } else { + files[index.row()].state = MissingFilesState::Replaced; + + if (loop) + fileCheckLoop(files, path, false); + } + + emit dataChanged(index, index); + success = true; + } + } + + return success; +} + +QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case MissingFilesColumn::State: + result = QTStr("MissingFiles.State"); + break; + case MissingFilesColumn::Source: + result = QTStr("Basic.Main.Source"); + break; + case MissingFilesColumn::OriginalPath: + result = QTStr("MissingFiles.MissingFile"); + break; + case MissingFilesColumn::NewPath: + result = QTStr("MissingFiles.NewFile"); + break; + } + } + + return result; +} + +OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) + : QDialog(parent), + filesModel(new MissingFilesModel), + ui(new Ui::OBSMissingFiles) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(filesModel); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, + new MissingFilesPathItemDelegate(false, "")); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, + new MissingFilesPathItemDelegate(true, "")); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); + + for (size_t i = 0; i < obs_missing_files_count(files); i++) { + obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); + + const char *oldPath = obs_missing_file_get_path(f); + const char *name = obs_missing_file_get_source_name(f); + + addMissingFile(oldPath, name); + } + + QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); + + ui->found->setText(found); + + fileStore = files; + + connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); + connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); + connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); + connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); + + QModelIndex index = filesModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +OBSMissingFiles::~OBSMissingFiles() +{ + obs_missing_files_destroy(fileStore); +} + +void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) +{ + QStringList list; + + list.append(originalPath); + list.append(sourceName); + + QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); + + filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); +} + +void OBSMissingFiles::saveFiles() +{ + for (int i = 0; i < filesModel->files.length(); i++) { + MissingFilesState state = filesModel->files[i].state; + if (state != MissingFilesState::Missing) { + obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); + + QString path = filesModel->files[i].newPath; + + if (state == MissingFilesState::Cleared) { + obs_missing_file_issue_callback(f, ""); + } else { + char *p = bstrdup(path.toStdString().c_str()); + obs_missing_file_issue_callback(f, p); + bfree(p); + } + } + } + + QDialog::accept(); +} + +void OBSMissingFiles::browseFolders() +{ + QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir != "") { + dir += "/"; + filesModel->fileCheckLoop(filesModel->files, dir, true); + } +} + +void OBSMissingFiles::dataChanged() +{ + QString found = + QTStr("MissingFiles.NumFound") + .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); + + ui->found->setText(found); + + ui->tableView->resizeColumnToContents(MissingFilesColumn::State); + ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); +} + +QIcon OBSMissingFiles::GetWarningIcon() +{ + return filesModel->warningIcon; +} + +void OBSMissingFiles::SetWarningIcon(const QIcon &icon) +{ + ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); + filesModel->warningIcon = icon; +} diff --git a/frontend/utility/MissingFilesModel.hpp b/frontend/utility/MissingFilesModel.hpp new file mode 100644 index 000000000..2b24e0f8e --- /dev/null +++ b/frontend/utility/MissingFilesModel.hpp @@ -0,0 +1,112 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + 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 +#include +#include "obs-app.hpp" +#include "ui_OBSMissingFiles.h" + +class MissingFilesModel; + +enum MissingFilesState { Missing, Found, Replaced, Cleared }; +Q_DECLARE_METATYPE(MissingFilesState); + +class OBSMissingFiles : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) + + QPointer filesModel; + std::unique_ptr ui; + +public: + explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); + virtual ~OBSMissingFiles() override; + + void addMissingFile(const char *originalPath, const char *sourceName); + + QIcon GetWarningIcon(); + void SetWarningIcon(const QIcon &icon); + +private: + void saveFiles(); + void browseFolders(); + + obs_missing_files_t *fileStore; + +public slots: + void dataChanged(); +}; + +class MissingFilesModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSMissingFiles; + +public: + explicit MissingFilesModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int found() const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + bool loop = true; + + QIcon warningIcon; + +private: + struct MissingFileEntry { + MissingFilesState state = MissingFilesState::Missing; + + QString source; + + QString originalPath; + QString newPath; + }; + + QList files; + + void fileCheckLoop(QList files, QString path, bool skipPrompt); +}; + +class MissingFilesPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); +}; diff --git a/frontend/utility/MissingFilesPathItemDelegate.cpp b/frontend/utility/MissingFilesPathItemDelegate.cpp new file mode 100644 index 000000000..57e641a74 --- /dev/null +++ b/frontend/utility/MissingFilesPathItemDelegate.cpp @@ -0,0 +1,542 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + 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 "moc_window-missing-files.cpp" +#include "window-basic-main.hpp" + +#include "obs-app.hpp" + +#include +#include +#include + +#include + +enum MissingFilesColumn { + Source, + OriginalPath, + NewPath, + State, + + Count +}; + +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &) const +{ + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in input cells + if (isOutput) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + + return container; +} + +void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + model->setData(index, list); + } else + model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text(), 0); + } +} + +void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) +{ + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = + QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); + +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } + + if (isSet) + emit commitData(container); +} + +void MissingFilesPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); + container->findChild()->clearFocus(); + ((QWidget *)container->parent())->setFocus(); + emit commitData(container); +} + +/** + Model +**/ + +MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) +{ + QStyle *style = QApplication::style(); + + warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); +} + +int MissingFilesModel::rowCount(const QModelIndex &) const +{ + return files.length(); +} + +int MissingFilesModel::columnCount(const QModelIndex &) const +{ + return MissingFilesColumn::Count; +} + +int MissingFilesModel::found() const +{ + int res = 0; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != Missing && files[i].state != Cleared) + res++; + } + + return res; +} + +QVariant MissingFilesModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= files.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + QFileInfo fi(files[index.row()].originalPath); + + switch (index.column()) { + case MissingFilesColumn::Source: + result = files[index.row()].source; + break; + case MissingFilesColumn::OriginalPath: + result = fi.fileName(); + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + case MissingFilesColumn::State: + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + } + break; + } + } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); + + if (source) { + result = main->GetSourceIcon(obs_source_get_id(source)); + } + } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { + QFont font = QFont(); + font.setBold(true); + + result = font; + } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + + default: + break; + } + } else if (role == Qt::ToolTipRole) { + switch (index.column()) { + case MissingFilesColumn::OriginalPath: + result = files[index.row()].originalPath; + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + default: + break; + } + } + + return result; +} + +Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == MissingFilesColumn::OriginalPath) { + flags &= ~Qt::ItemIsEditable; + } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) +{ + loop = false; + QUrl url = QUrl().fromLocalFile(path); + QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); + + bool prompted = skipPrompt; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != MissingFilesState::Missing) + continue; + + QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); + QString filename = origFile.fileName(); + QString testFile = dir + filename; + + if (os_file_exists(testFile.toStdString().c_str())) { + if (!prompted) { + QMessageBox::StandardButton button = + QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), + QTStr("MissingFiles.AutoSearchText")); + + if (button == QMessageBox::No) + break; + + prompted = true; + } + QModelIndex in = index(i, MissingFilesColumn::NewPath); + setData(in, testFile, 0); + } + } + loop = true; +} + +bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == MissingFilesRole::NewPathsToProcessRole) { + QStringList list = value.toStringList(); + + int row = index.row() + 1; + beginInsertRows(QModelIndex(), row, row); + + MissingFileEntry entry; + entry.originalPath = list[0].replace("\\", "/"); + entry.source = list[1]; + + files.insert(row, entry); + row++; + + endInsertRows(); + + success = true; + } else { + QString path = value.toString(); + if (index.column() == MissingFilesColumn::NewPath) { + files[index.row()].newPath = value.toString(); + QString fileName = QUrl(path).fileName(); + QString origFileName = QUrl(files[index.row()].originalPath).fileName(); + + if (path.isEmpty()) { + files[index.row()].state = MissingFilesState::Missing; + } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { + files[index.row()].state = MissingFilesState::Cleared; + } else if (fileName.compare(origFileName) == 0) { + files[index.row()].state = MissingFilesState::Found; + + if (loop) + fileCheckLoop(files, path, false); + } else { + files[index.row()].state = MissingFilesState::Replaced; + + if (loop) + fileCheckLoop(files, path, false); + } + + emit dataChanged(index, index); + success = true; + } + } + + return success; +} + +QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case MissingFilesColumn::State: + result = QTStr("MissingFiles.State"); + break; + case MissingFilesColumn::Source: + result = QTStr("Basic.Main.Source"); + break; + case MissingFilesColumn::OriginalPath: + result = QTStr("MissingFiles.MissingFile"); + break; + case MissingFilesColumn::NewPath: + result = QTStr("MissingFiles.NewFile"); + break; + } + } + + return result; +} + +OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) + : QDialog(parent), + filesModel(new MissingFilesModel), + ui(new Ui::OBSMissingFiles) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(filesModel); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, + new MissingFilesPathItemDelegate(false, "")); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, + new MissingFilesPathItemDelegate(true, "")); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); + + for (size_t i = 0; i < obs_missing_files_count(files); i++) { + obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); + + const char *oldPath = obs_missing_file_get_path(f); + const char *name = obs_missing_file_get_source_name(f); + + addMissingFile(oldPath, name); + } + + QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); + + ui->found->setText(found); + + fileStore = files; + + connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); + connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); + connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); + connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); + + QModelIndex index = filesModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +OBSMissingFiles::~OBSMissingFiles() +{ + obs_missing_files_destroy(fileStore); +} + +void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) +{ + QStringList list; + + list.append(originalPath); + list.append(sourceName); + + QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); + + filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); +} + +void OBSMissingFiles::saveFiles() +{ + for (int i = 0; i < filesModel->files.length(); i++) { + MissingFilesState state = filesModel->files[i].state; + if (state != MissingFilesState::Missing) { + obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); + + QString path = filesModel->files[i].newPath; + + if (state == MissingFilesState::Cleared) { + obs_missing_file_issue_callback(f, ""); + } else { + char *p = bstrdup(path.toStdString().c_str()); + obs_missing_file_issue_callback(f, p); + bfree(p); + } + } + } + + QDialog::accept(); +} + +void OBSMissingFiles::browseFolders() +{ + QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir != "") { + dir += "/"; + filesModel->fileCheckLoop(filesModel->files, dir, true); + } +} + +void OBSMissingFiles::dataChanged() +{ + QString found = + QTStr("MissingFiles.NumFound") + .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); + + ui->found->setText(found); + + ui->tableView->resizeColumnToContents(MissingFilesColumn::State); + ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); +} + +QIcon OBSMissingFiles::GetWarningIcon() +{ + return filesModel->warningIcon; +} + +void OBSMissingFiles::SetWarningIcon(const QIcon &icon) +{ + ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); + filesModel->warningIcon = icon; +} diff --git a/frontend/utility/MissingFilesPathItemDelegate.hpp b/frontend/utility/MissingFilesPathItemDelegate.hpp new file mode 100644 index 000000000..2b24e0f8e --- /dev/null +++ b/frontend/utility/MissingFilesPathItemDelegate.hpp @@ -0,0 +1,112 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + 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 +#include +#include "obs-app.hpp" +#include "ui_OBSMissingFiles.h" + +class MissingFilesModel; + +enum MissingFilesState { Missing, Found, Replaced, Cleared }; +Q_DECLARE_METATYPE(MissingFilesState); + +class OBSMissingFiles : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) + + QPointer filesModel; + std::unique_ptr ui; + +public: + explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); + virtual ~OBSMissingFiles() override; + + void addMissingFile(const char *originalPath, const char *sourceName); + + QIcon GetWarningIcon(); + void SetWarningIcon(const QIcon &icon); + +private: + void saveFiles(); + void browseFolders(); + + obs_missing_files_t *fileStore; + +public slots: + void dataChanged(); +}; + +class MissingFilesModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSMissingFiles; + +public: + explicit MissingFilesModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int found() const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + bool loop = true; + + QIcon warningIcon; + +private: + struct MissingFileEntry { + MissingFilesState state = MissingFilesState::Missing; + + QString source; + + QString originalPath; + QString newPath; + }; + + QList files; + + void fileCheckLoop(QList files, QString path, bool skipPrompt); +}; + +class MissingFilesPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); +}; diff --git a/frontend/utility/OBSEventFilter.hpp b/frontend/utility/OBSEventFilter.hpp new file mode 100644 index 000000000..6bfaaab4a --- /dev/null +++ b/frontend/utility/OBSEventFilter.hpp @@ -0,0 +1,82 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include + +#include +#include + +class OBSBasic; + +#include "ui_OBSBasicInteraction.h" + +class OBSEventFilter; + +class OBSBasicInteraction : public QDialog { + Q_OBJECT + +private: + OBSBasic *main; + + std::unique_ptr ui; + OBSSource source; + OBSSignal removedSignal; + OBSSignal renamedSignal; + std::unique_ptr eventFilter; + + static void SourceRemoved(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void DrawPreview(void *data, uint32_t cx, uint32_t cy); + + bool GetSourceRelativeXY(int mouseX, int mouseY, int &x, int &y); + + bool HandleMouseClickEvent(QMouseEvent *event); + bool HandleMouseMoveEvent(QMouseEvent *event); + bool HandleMouseWheelEvent(QWheelEvent *event); + bool HandleFocusEvent(QFocusEvent *event); + bool HandleKeyEvent(QKeyEvent *event); + + OBSEventFilter *BuildEventFilter(); + +public: + OBSBasicInteraction(QWidget *parent, OBSSource source_); + ~OBSBasicInteraction(); + + void Init(); + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; +}; + +typedef std::function EventFilterFunc; + +class OBSEventFilter : public QObject { + Q_OBJECT +public: + OBSEventFilter(EventFilterFunc filter_) : filter(filter_) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) { return filter(obj, event); } + +public: + EventFilterFunc filter; +}; diff --git a/frontend/utility/RemuxEntryPathItemDelegate.cpp b/frontend/utility/RemuxEntryPathItemDelegate.cpp new file mode 100644 index 000000000..d72ebbd9c --- /dev/null +++ b/frontend/utility/RemuxEntryPathItemDelegate.cpp @@ -0,0 +1,924 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 "moc_window-remux.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-basic-main.hpp" + +#include +#include + +using namespace std; + +enum RemuxEntryColumn { + State, + InputPath, + OutputPath, + + Count +}; + +enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { + // Never allow modification of rows that are + // in progress. + return Q_NULLPTR; + } else if (isOutput && state != RemuxEntryState::Ready) { + // Do not allow modification of output rows + // that aren't associated with a valid input. + return Q_NULLPTR; + } else if (!isOutput && state == RemuxEntryState::Complete) { + // Don't allow modification of rows that are + // already complete. + return Q_NULLPTR; + } else { + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!isOutput && state != RemuxEntryState::Empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; + } +} + +void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + if (list.size() > 0) + model->setData(index, list); + } else + model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + if (isOutput) { + if (state != Ready) { + QColor background = + localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); + + localOption.backgroundBrush = QBrush(background); + } + } + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty()) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } else { + QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, + QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + } + + if (isSet) + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/********************************************************** + Model - Manages the queue's data +**********************************************************/ + +int RemuxQueueModel::rowCount(const QModelIndex &) const +{ + return queue.length() + (isProcessing ? 0 : 1); +} + +int RemuxQueueModel::columnCount(const QModelIndex &) const +{ + return RemuxEntryColumn::Count; +} + +QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= queue.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + result = queue[index.row()].sourcePath; + break; + case RemuxEntryColumn::OutputPath: + result = queue[index.row()].targetPath; + break; + } + } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { + result = getIcon(queue[index.row()].state); + } else if (role == RemuxEntryRole::EntryStateRole) { + result = queue[index.row()].state; + } + + return result; +} + +QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case RemuxEntryColumn::State: + result = QString(); + break; + case RemuxEntryColumn::InputPath: + result = QTStr("Remux.SourceFile"); + break; + case RemuxEntryColumn::OutputPath: + result = QTStr("Remux.TargetFile"); + break; + } + } + + return result; +} + +Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == RemuxEntryColumn::InputPath) { + flags |= Qt::ItemIsEditable; + } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == RemuxEntryRole::NewPathsToProcessRole) { + QStringList pathList = value.toStringList(); + + if (pathList.size() == 0) { + if (index.row() < queue.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (pathList.size() >= 1 && index.row() < queue.length()) { + queue[index.row()].sourcePath = pathList[0]; + checkInputPath(index.row()); + + pathList.removeAt(0); + + success = true; + } + + if (pathList.size() > 0) { + int row = index.row(); + int lastRow = row + pathList.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : pathList) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + queue.insert(row, entry); + row++; + } + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + + success = true; + } + } + } else if (index.row() == queue.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); + queue.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + success = true; + } + } else { + QString path = value.toString(); + + if (path.isEmpty()) { + if (index.column() == RemuxEntryColumn::InputPath) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + queue[index.row()].sourcePath = value.toString(); + checkInputPath(index.row()); + success = true; + break; + case RemuxEntryColumn::OutputPath: + queue[index.row()].targetPath = value.toString(); + emit dataChanged(index, index); + success = true; + break; + } + } + } + + return success; +} + +QVariant RemuxQueueModel::getIcon(RemuxEntryState state) +{ + QVariant icon; + QStyle *style = QApplication::style(); + + switch (state) { + case RemuxEntryState::Complete: + icon = style->standardIcon(QStyle::SP_DialogApplyButton); + break; + + case RemuxEntryState::InProgress: + icon = style->standardIcon(QStyle::SP_ArrowRight); + break; + + case RemuxEntryState::Error: + icon = style->standardIcon(QStyle::SP_DialogCancelButton); + break; + + case RemuxEntryState::InvalidPath: + icon = style->standardIcon(QStyle::SP_MessageBoxWarning); + break; + + default: + break; + } + + return icon; +} + +void RemuxQueueModel::checkInputPath(int row) +{ + RemuxQueueEntry &entry = queue[row]; + + if (entry.sourcePath.isEmpty()) { + entry.state = RemuxEntryState::Empty; + } else { + entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); + QFileInfo fileInfo(entry.sourcePath); + if (fileInfo.exists()) + entry.state = RemuxEntryState::Ready; + else + entry.state = RemuxEntryState::InvalidPath; + + QString newExt = ".mp4"; + QString suffix = fileInfo.suffix(); + + if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { + newExt = ".remuxed." + suffix; + } + + if (entry.state == RemuxEntryState::Ready) + entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + + fileInfo.completeBaseName() + newExt); + } + + if (entry.state == RemuxEntryState::Ready && isProcessing) + entry.state = RemuxEntryState::Pending; + + emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); +} + +QFileInfoList RemuxQueueModel::checkForOverwrites() const +{ + QFileInfoList list; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Ready) { + QFileInfo fileInfo(entry.targetPath); + if (fileInfo.exists()) { + list.append(fileInfo); + } + } + } + + return list; +} + +bool RemuxQueueModel::checkForErrors() const +{ + bool hasErrors = false; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Error) { + hasErrors = true; + break; + } + } + + return hasErrors; +} + +void RemuxQueueModel::clearAll() +{ + beginRemoveRows(QModelIndex(), 0, queue.size() - 1); + queue.clear(); + endRemoveRows(); +} + +void RemuxQueueModel::clearFinished() +{ + int index = 0; + + for (index = 0; index < queue.size(); index++) { + const RemuxQueueEntry &entry = queue[index]; + if (entry.state == RemuxEntryState::Complete) { + beginRemoveRows(QModelIndex(), index, index); + queue.removeAt(index); + endRemoveRows(); + index--; + } + } +} + +bool RemuxQueueModel::canClearFinished() const +{ + bool canClearFinished = false; + for (const RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Complete) { + canClearFinished = true; + break; + } + + return canClearFinished; +} + +void RemuxQueueModel::beginProcessing() +{ + for (RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Ready) + entry.state = RemuxEntryState::Pending; + + // Signal that the insertion point no longer exists. + beginRemoveRows(QModelIndex(), queue.length(), queue.length()); + endRemoveRows(); + + isProcessing = true; + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +void RemuxQueueModel::endProcessing() +{ + for (RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::Ready; + } + } + + // Signal that the insertion point exists again. + isProcessing = false; + if (!autoRemux) { + beginInsertRows(QModelIndex(), queue.length(), queue.length()); + endInsertRows(); + } + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) +{ + bool anyStarted = false; + + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::InProgress; + + inputPath = entry.sourcePath; + outputPath = entry.targetPath; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + anyStarted = true; + break; + } + } + + return anyStarted; +} + +void RemuxQueueModel::finishEntry(bool success) +{ + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::InProgress) { + if (success) + entry.state = RemuxEntryState::Complete; + else + entry.state = RemuxEntryState::Error; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + break; + } + } +} + +/********************************************************** + The actual remux window implementation +**********************************************************/ + +OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) + : QDialog(parent), + queueModel(new RemuxQueueModel), + worker(new RemuxWorker()), + ui(new Ui::OBSRemux), + recPath(path), + autoRemux(autoRemux_) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + + if (autoRemux) { + resize(280, 40); + ui->tableView->hide(); + ui->buttonBox->hide(); + ui->label->hide(); + } + + ui->progressBar->setMinimum(0); + ui->progressBar->setMaximum(1000); + ui->progressBar->setValue(0); + + ui->tableView->setModel(queueModel); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, + new RemuxEntryPathItemDelegate(false, recPath)); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, + new RemuxEntryPathItemDelegate(true, recPath)); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, + QHeaderView::ResizeMode::Fixed); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + ui->tableView->setTextElideMode(Qt::ElideMiddle); + ui->tableView->setWordWrap(false); + + installEventFilter(CreateShortcutFilter()); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); + connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); + connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, + &OBSRemux::clearAll); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); + + worker->moveToThread(&remuxer); + remuxer.start(); + + connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); + connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); + connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); + connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); + + connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); + connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); + + QModelIndex index = queueModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +bool OBSRemux::stopRemux() +{ + if (!worker->isWorking) + return true; + + // By locking the worker thread's mutex, we ensure that its + // update poll will be blocked as long as we're in here with + // the popup open. + QMutexLocker lock(&worker->updateMutex); + + bool exit = false; + + if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + exit = true; + } + + if (exit) { + // Inform the worker it should no longer be + // working. It will interrupt accordingly in + // its next update callback. + worker->isWorking = false; + } + + return exit; +} + +OBSRemux::~OBSRemux() +{ + stopRemux(); + remuxer.quit(); + remuxer.wait(); +} + +void OBSRemux::rowCountChanged(const QModelIndex &, int, int) +{ + // See if there are still any rows ready to remux. Change + // the state of the "go" button accordingly. + // There must be more than one row, since there will always be + // at least one row for the empty insertion point. + if (queueModel->rowCount() > 1) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + } else { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + } +} + +void OBSRemux::dropEvent(QDropEvent *ev) +{ + QStringList urlList; + + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + + if (fileInfo.isDir()) { + QStringList directoryFilter; + directoryFilter << "*.flv" + << "*.mp4" + << "*.mov" + << "*.mkv" + << "*.ts" + << "*.m3u8"; + + QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, + QDirIterator::Subdirectories); + + while (dirIter.hasNext()) { + urlList.append(dirIter.next()); + } + } else { + urlList.append(fileInfo.canonicalFilePath()); + } + } + + if (urlList.empty()) { + QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), + QMessageBox::Ok); + } else if (!autoRemux) { + QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); + queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); + } +} + +void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls() && !worker->isWorking) + ev->accept(); +} + +void OBSRemux::beginRemux() +{ + if (worker->isWorking) { + stopRemux(); + return; + } + + bool proceedWithRemux = true; + QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); + + if (!overwriteFiles.empty()) { + QString message = QTStr("Remux.FileExists"); + message += "\n\n"; + + for (QFileInfo fileInfo : overwriteFiles) + message += fileInfo.canonicalFilePath() + "\n"; + + if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) + proceedWithRemux = false; + } + + if (!proceedWithRemux) + return; + + // Set all jobs to "pending" first. + queueModel->beginProcessing(); + + ui->progressBar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); + setAcceptDrops(false); + + remuxNextEntry(); +} + +void OBSRemux::AutoRemux(QString inFile, QString outFile) +{ + if (inFile != "" && outFile != "" && autoRemux) { + ui->progressBar->setVisible(true); + emit remux(inFile, outFile); + autoRemuxFile = outFile; + } +} + +void OBSRemux::remuxNextEntry() +{ + worker->lastProgress = 0.f; + + QString inputPath, outputPath; + if (queueModel->beginNextEntry(inputPath, outputPath)) { + emit remux(inputPath, outputPath); + } else { + queueModel->autoRemux = autoRemux; + queueModel->endProcessing(); + + if (!autoRemux) { + OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), + queueModel->checkForErrors() ? QTStr("Remux.FinishedError") + : QTStr("Remux.Finished")); + } + + ui->progressBar->setVisible(autoRemux); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + setAcceptDrops(true); + } +} + +void OBSRemux::closeEvent(QCloseEvent *event) +{ + if (!stopRemux()) + event->ignore(); + else + QDialog::closeEvent(event); +} + +void OBSRemux::reject() +{ + if (!stopRemux()) + return; + + QDialog::reject(); +} + +void OBSRemux::updateProgress(float percent) +{ + ui->progressBar->setValue(percent * 10); +} + +void OBSRemux::remuxFinished(bool success) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + + queueModel->finishEntry(success); + + if (autoRemux && autoRemuxFile != "") { + QTimer::singleShot(3000, this, &OBSRemux::close); + + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); + } + + remuxNextEntry(); +} + +void OBSRemux::clearFinished() +{ + queueModel->clearFinished(); +} + +void OBSRemux::clearAll() +{ + queueModel->clearAll(); +} + +/********************************************************** + Worker thread - Executes the libobs remux operation as a + background process. +**********************************************************/ + +void RemuxWorker::UpdateProgress(float percent) +{ + if (abs(lastProgress - percent) < 0.1f) + return; + + emit updateProgress(percent); + lastProgress = percent; +} + +void RemuxWorker::remux(const QString &source, const QString &target) +{ + isWorking = true; + + auto callback = [](void *data, float percent) { + RemuxWorker *rw = static_cast(data); + + QMutexLocker lock(&rw->updateMutex); + + rw->UpdateProgress(percent); + + return rw->isWorking; + }; + + bool stopped = false; + bool success = false; + + media_remux_job_t mr_job = nullptr; + if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { + + success = media_remux_job_process(mr_job, callback, this); + + media_remux_job_destroy(mr_job); + + stopped = !isWorking; + } + + isWorking = false; + + emit remuxFinished(!stopped && success); +} diff --git a/frontend/utility/RemuxEntryPathItemDelegate.hpp b/frontend/utility/RemuxEntryPathItemDelegate.hpp new file mode 100644 index 000000000..20a13a780 --- /dev/null +++ b/frontend/utility/RemuxEntryPathItemDelegate.hpp @@ -0,0 +1,173 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 +#include +#include +#include +#include +#include +#include "ui_OBSRemux.h" + +#include +#include + +class RemuxQueueModel; +class RemuxWorker; + +enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; +Q_DECLARE_METATYPE(RemuxEntryState); + +class OBSRemux : public QDialog { + Q_OBJECT + + QPointer queueModel; + QThread remuxer; + QPointer worker; + + std::unique_ptr ui; + + const char *recPath; + + virtual void closeEvent(QCloseEvent *event) override; + virtual void reject() override; + + bool autoRemux; + QString autoRemuxFile; + +public: + explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); + virtual ~OBSRemux() override; + + using job_t = std::shared_ptr; + + void AutoRemux(QString inFile, QString outFile); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + + void remuxNextEntry(); + +private slots: + void rowCountChanged(const QModelIndex &parent, int first, int last); + +public slots: + void updateProgress(float percent); + void remuxFinished(bool success); + void beginRemux(); + bool stopRemux(); + void clearFinished(); + void clearAll(); + +signals: + void remux(const QString &source, const QString &target); +}; + +class RemuxQueueModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSRemux; + +public: + RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + QFileInfoList checkForOverwrites() const; + bool checkForErrors() const; + void beginProcessing(); + void endProcessing(); + bool beginNextEntry(QString &inputPath, QString &outputPath); + void finishEntry(bool success); + bool canClearFinished() const; + void clearFinished(); + void clearAll(); + + bool autoRemux = false; + +private: + struct RemuxQueueEntry { + RemuxEntryState state; + + QString sourcePath; + QString targetPath; + }; + + QList queue; + bool isProcessing; + + static QVariant getIcon(RemuxEntryState state); + + void checkInputPath(int row); +}; + +class RemuxWorker : public QObject { + Q_OBJECT + + QMutex updateMutex; + + bool isWorking; + + float lastProgress; + void UpdateProgress(float percent); + + explicit RemuxWorker() : isWorking(false) {} + virtual ~RemuxWorker(){}; + +private slots: + void remux(const QString &source, const QString &target); + +signals: + void updateProgress(float percent); + void remuxFinished(bool success); + + friend class OBSRemux; +}; + +class RemuxEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/frontend/utility/RemuxQueueModel.cpp b/frontend/utility/RemuxQueueModel.cpp new file mode 100644 index 000000000..d72ebbd9c --- /dev/null +++ b/frontend/utility/RemuxQueueModel.cpp @@ -0,0 +1,924 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 "moc_window-remux.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-basic-main.hpp" + +#include +#include + +using namespace std; + +enum RemuxEntryColumn { + State, + InputPath, + OutputPath, + + Count +}; + +enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { + // Never allow modification of rows that are + // in progress. + return Q_NULLPTR; + } else if (isOutput && state != RemuxEntryState::Ready) { + // Do not allow modification of output rows + // that aren't associated with a valid input. + return Q_NULLPTR; + } else if (!isOutput && state == RemuxEntryState::Complete) { + // Don't allow modification of rows that are + // already complete. + return Q_NULLPTR; + } else { + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!isOutput && state != RemuxEntryState::Empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; + } +} + +void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + if (list.size() > 0) + model->setData(index, list); + } else + model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + if (isOutput) { + if (state != Ready) { + QColor background = + localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); + + localOption.backgroundBrush = QBrush(background); + } + } + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty()) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } else { + QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, + QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + } + + if (isSet) + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/********************************************************** + Model - Manages the queue's data +**********************************************************/ + +int RemuxQueueModel::rowCount(const QModelIndex &) const +{ + return queue.length() + (isProcessing ? 0 : 1); +} + +int RemuxQueueModel::columnCount(const QModelIndex &) const +{ + return RemuxEntryColumn::Count; +} + +QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= queue.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + result = queue[index.row()].sourcePath; + break; + case RemuxEntryColumn::OutputPath: + result = queue[index.row()].targetPath; + break; + } + } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { + result = getIcon(queue[index.row()].state); + } else if (role == RemuxEntryRole::EntryStateRole) { + result = queue[index.row()].state; + } + + return result; +} + +QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case RemuxEntryColumn::State: + result = QString(); + break; + case RemuxEntryColumn::InputPath: + result = QTStr("Remux.SourceFile"); + break; + case RemuxEntryColumn::OutputPath: + result = QTStr("Remux.TargetFile"); + break; + } + } + + return result; +} + +Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == RemuxEntryColumn::InputPath) { + flags |= Qt::ItemIsEditable; + } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == RemuxEntryRole::NewPathsToProcessRole) { + QStringList pathList = value.toStringList(); + + if (pathList.size() == 0) { + if (index.row() < queue.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (pathList.size() >= 1 && index.row() < queue.length()) { + queue[index.row()].sourcePath = pathList[0]; + checkInputPath(index.row()); + + pathList.removeAt(0); + + success = true; + } + + if (pathList.size() > 0) { + int row = index.row(); + int lastRow = row + pathList.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : pathList) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + queue.insert(row, entry); + row++; + } + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + + success = true; + } + } + } else if (index.row() == queue.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); + queue.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + success = true; + } + } else { + QString path = value.toString(); + + if (path.isEmpty()) { + if (index.column() == RemuxEntryColumn::InputPath) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + queue[index.row()].sourcePath = value.toString(); + checkInputPath(index.row()); + success = true; + break; + case RemuxEntryColumn::OutputPath: + queue[index.row()].targetPath = value.toString(); + emit dataChanged(index, index); + success = true; + break; + } + } + } + + return success; +} + +QVariant RemuxQueueModel::getIcon(RemuxEntryState state) +{ + QVariant icon; + QStyle *style = QApplication::style(); + + switch (state) { + case RemuxEntryState::Complete: + icon = style->standardIcon(QStyle::SP_DialogApplyButton); + break; + + case RemuxEntryState::InProgress: + icon = style->standardIcon(QStyle::SP_ArrowRight); + break; + + case RemuxEntryState::Error: + icon = style->standardIcon(QStyle::SP_DialogCancelButton); + break; + + case RemuxEntryState::InvalidPath: + icon = style->standardIcon(QStyle::SP_MessageBoxWarning); + break; + + default: + break; + } + + return icon; +} + +void RemuxQueueModel::checkInputPath(int row) +{ + RemuxQueueEntry &entry = queue[row]; + + if (entry.sourcePath.isEmpty()) { + entry.state = RemuxEntryState::Empty; + } else { + entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); + QFileInfo fileInfo(entry.sourcePath); + if (fileInfo.exists()) + entry.state = RemuxEntryState::Ready; + else + entry.state = RemuxEntryState::InvalidPath; + + QString newExt = ".mp4"; + QString suffix = fileInfo.suffix(); + + if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { + newExt = ".remuxed." + suffix; + } + + if (entry.state == RemuxEntryState::Ready) + entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + + fileInfo.completeBaseName() + newExt); + } + + if (entry.state == RemuxEntryState::Ready && isProcessing) + entry.state = RemuxEntryState::Pending; + + emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); +} + +QFileInfoList RemuxQueueModel::checkForOverwrites() const +{ + QFileInfoList list; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Ready) { + QFileInfo fileInfo(entry.targetPath); + if (fileInfo.exists()) { + list.append(fileInfo); + } + } + } + + return list; +} + +bool RemuxQueueModel::checkForErrors() const +{ + bool hasErrors = false; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Error) { + hasErrors = true; + break; + } + } + + return hasErrors; +} + +void RemuxQueueModel::clearAll() +{ + beginRemoveRows(QModelIndex(), 0, queue.size() - 1); + queue.clear(); + endRemoveRows(); +} + +void RemuxQueueModel::clearFinished() +{ + int index = 0; + + for (index = 0; index < queue.size(); index++) { + const RemuxQueueEntry &entry = queue[index]; + if (entry.state == RemuxEntryState::Complete) { + beginRemoveRows(QModelIndex(), index, index); + queue.removeAt(index); + endRemoveRows(); + index--; + } + } +} + +bool RemuxQueueModel::canClearFinished() const +{ + bool canClearFinished = false; + for (const RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Complete) { + canClearFinished = true; + break; + } + + return canClearFinished; +} + +void RemuxQueueModel::beginProcessing() +{ + for (RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Ready) + entry.state = RemuxEntryState::Pending; + + // Signal that the insertion point no longer exists. + beginRemoveRows(QModelIndex(), queue.length(), queue.length()); + endRemoveRows(); + + isProcessing = true; + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +void RemuxQueueModel::endProcessing() +{ + for (RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::Ready; + } + } + + // Signal that the insertion point exists again. + isProcessing = false; + if (!autoRemux) { + beginInsertRows(QModelIndex(), queue.length(), queue.length()); + endInsertRows(); + } + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) +{ + bool anyStarted = false; + + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::InProgress; + + inputPath = entry.sourcePath; + outputPath = entry.targetPath; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + anyStarted = true; + break; + } + } + + return anyStarted; +} + +void RemuxQueueModel::finishEntry(bool success) +{ + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::InProgress) { + if (success) + entry.state = RemuxEntryState::Complete; + else + entry.state = RemuxEntryState::Error; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + break; + } + } +} + +/********************************************************** + The actual remux window implementation +**********************************************************/ + +OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) + : QDialog(parent), + queueModel(new RemuxQueueModel), + worker(new RemuxWorker()), + ui(new Ui::OBSRemux), + recPath(path), + autoRemux(autoRemux_) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + + if (autoRemux) { + resize(280, 40); + ui->tableView->hide(); + ui->buttonBox->hide(); + ui->label->hide(); + } + + ui->progressBar->setMinimum(0); + ui->progressBar->setMaximum(1000); + ui->progressBar->setValue(0); + + ui->tableView->setModel(queueModel); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, + new RemuxEntryPathItemDelegate(false, recPath)); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, + new RemuxEntryPathItemDelegate(true, recPath)); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, + QHeaderView::ResizeMode::Fixed); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + ui->tableView->setTextElideMode(Qt::ElideMiddle); + ui->tableView->setWordWrap(false); + + installEventFilter(CreateShortcutFilter()); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); + connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); + connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, + &OBSRemux::clearAll); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); + + worker->moveToThread(&remuxer); + remuxer.start(); + + connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); + connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); + connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); + connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); + + connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); + connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); + + QModelIndex index = queueModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +bool OBSRemux::stopRemux() +{ + if (!worker->isWorking) + return true; + + // By locking the worker thread's mutex, we ensure that its + // update poll will be blocked as long as we're in here with + // the popup open. + QMutexLocker lock(&worker->updateMutex); + + bool exit = false; + + if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + exit = true; + } + + if (exit) { + // Inform the worker it should no longer be + // working. It will interrupt accordingly in + // its next update callback. + worker->isWorking = false; + } + + return exit; +} + +OBSRemux::~OBSRemux() +{ + stopRemux(); + remuxer.quit(); + remuxer.wait(); +} + +void OBSRemux::rowCountChanged(const QModelIndex &, int, int) +{ + // See if there are still any rows ready to remux. Change + // the state of the "go" button accordingly. + // There must be more than one row, since there will always be + // at least one row for the empty insertion point. + if (queueModel->rowCount() > 1) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + } else { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + } +} + +void OBSRemux::dropEvent(QDropEvent *ev) +{ + QStringList urlList; + + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + + if (fileInfo.isDir()) { + QStringList directoryFilter; + directoryFilter << "*.flv" + << "*.mp4" + << "*.mov" + << "*.mkv" + << "*.ts" + << "*.m3u8"; + + QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, + QDirIterator::Subdirectories); + + while (dirIter.hasNext()) { + urlList.append(dirIter.next()); + } + } else { + urlList.append(fileInfo.canonicalFilePath()); + } + } + + if (urlList.empty()) { + QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), + QMessageBox::Ok); + } else if (!autoRemux) { + QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); + queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); + } +} + +void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls() && !worker->isWorking) + ev->accept(); +} + +void OBSRemux::beginRemux() +{ + if (worker->isWorking) { + stopRemux(); + return; + } + + bool proceedWithRemux = true; + QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); + + if (!overwriteFiles.empty()) { + QString message = QTStr("Remux.FileExists"); + message += "\n\n"; + + for (QFileInfo fileInfo : overwriteFiles) + message += fileInfo.canonicalFilePath() + "\n"; + + if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) + proceedWithRemux = false; + } + + if (!proceedWithRemux) + return; + + // Set all jobs to "pending" first. + queueModel->beginProcessing(); + + ui->progressBar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); + setAcceptDrops(false); + + remuxNextEntry(); +} + +void OBSRemux::AutoRemux(QString inFile, QString outFile) +{ + if (inFile != "" && outFile != "" && autoRemux) { + ui->progressBar->setVisible(true); + emit remux(inFile, outFile); + autoRemuxFile = outFile; + } +} + +void OBSRemux::remuxNextEntry() +{ + worker->lastProgress = 0.f; + + QString inputPath, outputPath; + if (queueModel->beginNextEntry(inputPath, outputPath)) { + emit remux(inputPath, outputPath); + } else { + queueModel->autoRemux = autoRemux; + queueModel->endProcessing(); + + if (!autoRemux) { + OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), + queueModel->checkForErrors() ? QTStr("Remux.FinishedError") + : QTStr("Remux.Finished")); + } + + ui->progressBar->setVisible(autoRemux); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + setAcceptDrops(true); + } +} + +void OBSRemux::closeEvent(QCloseEvent *event) +{ + if (!stopRemux()) + event->ignore(); + else + QDialog::closeEvent(event); +} + +void OBSRemux::reject() +{ + if (!stopRemux()) + return; + + QDialog::reject(); +} + +void OBSRemux::updateProgress(float percent) +{ + ui->progressBar->setValue(percent * 10); +} + +void OBSRemux::remuxFinished(bool success) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + + queueModel->finishEntry(success); + + if (autoRemux && autoRemuxFile != "") { + QTimer::singleShot(3000, this, &OBSRemux::close); + + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); + } + + remuxNextEntry(); +} + +void OBSRemux::clearFinished() +{ + queueModel->clearFinished(); +} + +void OBSRemux::clearAll() +{ + queueModel->clearAll(); +} + +/********************************************************** + Worker thread - Executes the libobs remux operation as a + background process. +**********************************************************/ + +void RemuxWorker::UpdateProgress(float percent) +{ + if (abs(lastProgress - percent) < 0.1f) + return; + + emit updateProgress(percent); + lastProgress = percent; +} + +void RemuxWorker::remux(const QString &source, const QString &target) +{ + isWorking = true; + + auto callback = [](void *data, float percent) { + RemuxWorker *rw = static_cast(data); + + QMutexLocker lock(&rw->updateMutex); + + rw->UpdateProgress(percent); + + return rw->isWorking; + }; + + bool stopped = false; + bool success = false; + + media_remux_job_t mr_job = nullptr; + if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { + + success = media_remux_job_process(mr_job, callback, this); + + media_remux_job_destroy(mr_job); + + stopped = !isWorking; + } + + isWorking = false; + + emit remuxFinished(!stopped && success); +} diff --git a/frontend/utility/RemuxQueueModel.hpp b/frontend/utility/RemuxQueueModel.hpp new file mode 100644 index 000000000..20a13a780 --- /dev/null +++ b/frontend/utility/RemuxQueueModel.hpp @@ -0,0 +1,173 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 +#include +#include +#include +#include +#include +#include "ui_OBSRemux.h" + +#include +#include + +class RemuxQueueModel; +class RemuxWorker; + +enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; +Q_DECLARE_METATYPE(RemuxEntryState); + +class OBSRemux : public QDialog { + Q_OBJECT + + QPointer queueModel; + QThread remuxer; + QPointer worker; + + std::unique_ptr ui; + + const char *recPath; + + virtual void closeEvent(QCloseEvent *event) override; + virtual void reject() override; + + bool autoRemux; + QString autoRemuxFile; + +public: + explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); + virtual ~OBSRemux() override; + + using job_t = std::shared_ptr; + + void AutoRemux(QString inFile, QString outFile); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + + void remuxNextEntry(); + +private slots: + void rowCountChanged(const QModelIndex &parent, int first, int last); + +public slots: + void updateProgress(float percent); + void remuxFinished(bool success); + void beginRemux(); + bool stopRemux(); + void clearFinished(); + void clearAll(); + +signals: + void remux(const QString &source, const QString &target); +}; + +class RemuxQueueModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSRemux; + +public: + RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + QFileInfoList checkForOverwrites() const; + bool checkForErrors() const; + void beginProcessing(); + void endProcessing(); + bool beginNextEntry(QString &inputPath, QString &outputPath); + void finishEntry(bool success); + bool canClearFinished() const; + void clearFinished(); + void clearAll(); + + bool autoRemux = false; + +private: + struct RemuxQueueEntry { + RemuxEntryState state; + + QString sourcePath; + QString targetPath; + }; + + QList queue; + bool isProcessing; + + static QVariant getIcon(RemuxEntryState state); + + void checkInputPath(int row); +}; + +class RemuxWorker : public QObject { + Q_OBJECT + + QMutex updateMutex; + + bool isWorking; + + float lastProgress; + void UpdateProgress(float percent); + + explicit RemuxWorker() : isWorking(false) {} + virtual ~RemuxWorker(){}; + +private slots: + void remux(const QString &source, const QString &target); + +signals: + void updateProgress(float percent); + void remuxFinished(bool success); + + friend class OBSRemux; +}; + +class RemuxEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/frontend/utility/RemuxWorker.cpp b/frontend/utility/RemuxWorker.cpp new file mode 100644 index 000000000..d72ebbd9c --- /dev/null +++ b/frontend/utility/RemuxWorker.cpp @@ -0,0 +1,924 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 "moc_window-remux.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-basic-main.hpp" + +#include +#include + +using namespace std; + +enum RemuxEntryColumn { + State, + InputPath, + OutputPath, + + Count +}; + +enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { + // Never allow modification of rows that are + // in progress. + return Q_NULLPTR; + } else if (isOutput && state != RemuxEntryState::Ready) { + // Do not allow modification of output rows + // that aren't associated with a valid input. + return Q_NULLPTR; + } else if (!isOutput && state == RemuxEntryState::Complete) { + // Don't allow modification of rows that are + // already complete. + return Q_NULLPTR; + } else { + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!isOutput && state != RemuxEntryState::Empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; + } +} + +void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + if (list.size() > 0) + model->setData(index, list); + } else + model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + if (isOutput) { + if (state != Ready) { + QColor background = + localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); + + localOption.backgroundBrush = QBrush(background); + } + } + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty()) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } else { + QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, + QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + } + + if (isSet) + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/********************************************************** + Model - Manages the queue's data +**********************************************************/ + +int RemuxQueueModel::rowCount(const QModelIndex &) const +{ + return queue.length() + (isProcessing ? 0 : 1); +} + +int RemuxQueueModel::columnCount(const QModelIndex &) const +{ + return RemuxEntryColumn::Count; +} + +QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= queue.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + result = queue[index.row()].sourcePath; + break; + case RemuxEntryColumn::OutputPath: + result = queue[index.row()].targetPath; + break; + } + } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { + result = getIcon(queue[index.row()].state); + } else if (role == RemuxEntryRole::EntryStateRole) { + result = queue[index.row()].state; + } + + return result; +} + +QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case RemuxEntryColumn::State: + result = QString(); + break; + case RemuxEntryColumn::InputPath: + result = QTStr("Remux.SourceFile"); + break; + case RemuxEntryColumn::OutputPath: + result = QTStr("Remux.TargetFile"); + break; + } + } + + return result; +} + +Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == RemuxEntryColumn::InputPath) { + flags |= Qt::ItemIsEditable; + } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == RemuxEntryRole::NewPathsToProcessRole) { + QStringList pathList = value.toStringList(); + + if (pathList.size() == 0) { + if (index.row() < queue.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (pathList.size() >= 1 && index.row() < queue.length()) { + queue[index.row()].sourcePath = pathList[0]; + checkInputPath(index.row()); + + pathList.removeAt(0); + + success = true; + } + + if (pathList.size() > 0) { + int row = index.row(); + int lastRow = row + pathList.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : pathList) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + queue.insert(row, entry); + row++; + } + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + + success = true; + } + } + } else if (index.row() == queue.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); + queue.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + success = true; + } + } else { + QString path = value.toString(); + + if (path.isEmpty()) { + if (index.column() == RemuxEntryColumn::InputPath) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + queue[index.row()].sourcePath = value.toString(); + checkInputPath(index.row()); + success = true; + break; + case RemuxEntryColumn::OutputPath: + queue[index.row()].targetPath = value.toString(); + emit dataChanged(index, index); + success = true; + break; + } + } + } + + return success; +} + +QVariant RemuxQueueModel::getIcon(RemuxEntryState state) +{ + QVariant icon; + QStyle *style = QApplication::style(); + + switch (state) { + case RemuxEntryState::Complete: + icon = style->standardIcon(QStyle::SP_DialogApplyButton); + break; + + case RemuxEntryState::InProgress: + icon = style->standardIcon(QStyle::SP_ArrowRight); + break; + + case RemuxEntryState::Error: + icon = style->standardIcon(QStyle::SP_DialogCancelButton); + break; + + case RemuxEntryState::InvalidPath: + icon = style->standardIcon(QStyle::SP_MessageBoxWarning); + break; + + default: + break; + } + + return icon; +} + +void RemuxQueueModel::checkInputPath(int row) +{ + RemuxQueueEntry &entry = queue[row]; + + if (entry.sourcePath.isEmpty()) { + entry.state = RemuxEntryState::Empty; + } else { + entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); + QFileInfo fileInfo(entry.sourcePath); + if (fileInfo.exists()) + entry.state = RemuxEntryState::Ready; + else + entry.state = RemuxEntryState::InvalidPath; + + QString newExt = ".mp4"; + QString suffix = fileInfo.suffix(); + + if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { + newExt = ".remuxed." + suffix; + } + + if (entry.state == RemuxEntryState::Ready) + entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + + fileInfo.completeBaseName() + newExt); + } + + if (entry.state == RemuxEntryState::Ready && isProcessing) + entry.state = RemuxEntryState::Pending; + + emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); +} + +QFileInfoList RemuxQueueModel::checkForOverwrites() const +{ + QFileInfoList list; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Ready) { + QFileInfo fileInfo(entry.targetPath); + if (fileInfo.exists()) { + list.append(fileInfo); + } + } + } + + return list; +} + +bool RemuxQueueModel::checkForErrors() const +{ + bool hasErrors = false; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Error) { + hasErrors = true; + break; + } + } + + return hasErrors; +} + +void RemuxQueueModel::clearAll() +{ + beginRemoveRows(QModelIndex(), 0, queue.size() - 1); + queue.clear(); + endRemoveRows(); +} + +void RemuxQueueModel::clearFinished() +{ + int index = 0; + + for (index = 0; index < queue.size(); index++) { + const RemuxQueueEntry &entry = queue[index]; + if (entry.state == RemuxEntryState::Complete) { + beginRemoveRows(QModelIndex(), index, index); + queue.removeAt(index); + endRemoveRows(); + index--; + } + } +} + +bool RemuxQueueModel::canClearFinished() const +{ + bool canClearFinished = false; + for (const RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Complete) { + canClearFinished = true; + break; + } + + return canClearFinished; +} + +void RemuxQueueModel::beginProcessing() +{ + for (RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Ready) + entry.state = RemuxEntryState::Pending; + + // Signal that the insertion point no longer exists. + beginRemoveRows(QModelIndex(), queue.length(), queue.length()); + endRemoveRows(); + + isProcessing = true; + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +void RemuxQueueModel::endProcessing() +{ + for (RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::Ready; + } + } + + // Signal that the insertion point exists again. + isProcessing = false; + if (!autoRemux) { + beginInsertRows(QModelIndex(), queue.length(), queue.length()); + endInsertRows(); + } + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) +{ + bool anyStarted = false; + + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::InProgress; + + inputPath = entry.sourcePath; + outputPath = entry.targetPath; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + anyStarted = true; + break; + } + } + + return anyStarted; +} + +void RemuxQueueModel::finishEntry(bool success) +{ + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::InProgress) { + if (success) + entry.state = RemuxEntryState::Complete; + else + entry.state = RemuxEntryState::Error; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + break; + } + } +} + +/********************************************************** + The actual remux window implementation +**********************************************************/ + +OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) + : QDialog(parent), + queueModel(new RemuxQueueModel), + worker(new RemuxWorker()), + ui(new Ui::OBSRemux), + recPath(path), + autoRemux(autoRemux_) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + + if (autoRemux) { + resize(280, 40); + ui->tableView->hide(); + ui->buttonBox->hide(); + ui->label->hide(); + } + + ui->progressBar->setMinimum(0); + ui->progressBar->setMaximum(1000); + ui->progressBar->setValue(0); + + ui->tableView->setModel(queueModel); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, + new RemuxEntryPathItemDelegate(false, recPath)); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, + new RemuxEntryPathItemDelegate(true, recPath)); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, + QHeaderView::ResizeMode::Fixed); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + ui->tableView->setTextElideMode(Qt::ElideMiddle); + ui->tableView->setWordWrap(false); + + installEventFilter(CreateShortcutFilter()); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); + connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); + connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, + &OBSRemux::clearAll); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); + + worker->moveToThread(&remuxer); + remuxer.start(); + + connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); + connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); + connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); + connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); + + connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); + connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); + + QModelIndex index = queueModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +bool OBSRemux::stopRemux() +{ + if (!worker->isWorking) + return true; + + // By locking the worker thread's mutex, we ensure that its + // update poll will be blocked as long as we're in here with + // the popup open. + QMutexLocker lock(&worker->updateMutex); + + bool exit = false; + + if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + exit = true; + } + + if (exit) { + // Inform the worker it should no longer be + // working. It will interrupt accordingly in + // its next update callback. + worker->isWorking = false; + } + + return exit; +} + +OBSRemux::~OBSRemux() +{ + stopRemux(); + remuxer.quit(); + remuxer.wait(); +} + +void OBSRemux::rowCountChanged(const QModelIndex &, int, int) +{ + // See if there are still any rows ready to remux. Change + // the state of the "go" button accordingly. + // There must be more than one row, since there will always be + // at least one row for the empty insertion point. + if (queueModel->rowCount() > 1) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + } else { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + } +} + +void OBSRemux::dropEvent(QDropEvent *ev) +{ + QStringList urlList; + + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + + if (fileInfo.isDir()) { + QStringList directoryFilter; + directoryFilter << "*.flv" + << "*.mp4" + << "*.mov" + << "*.mkv" + << "*.ts" + << "*.m3u8"; + + QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, + QDirIterator::Subdirectories); + + while (dirIter.hasNext()) { + urlList.append(dirIter.next()); + } + } else { + urlList.append(fileInfo.canonicalFilePath()); + } + } + + if (urlList.empty()) { + QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), + QMessageBox::Ok); + } else if (!autoRemux) { + QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); + queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); + } +} + +void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls() && !worker->isWorking) + ev->accept(); +} + +void OBSRemux::beginRemux() +{ + if (worker->isWorking) { + stopRemux(); + return; + } + + bool proceedWithRemux = true; + QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); + + if (!overwriteFiles.empty()) { + QString message = QTStr("Remux.FileExists"); + message += "\n\n"; + + for (QFileInfo fileInfo : overwriteFiles) + message += fileInfo.canonicalFilePath() + "\n"; + + if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) + proceedWithRemux = false; + } + + if (!proceedWithRemux) + return; + + // Set all jobs to "pending" first. + queueModel->beginProcessing(); + + ui->progressBar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); + setAcceptDrops(false); + + remuxNextEntry(); +} + +void OBSRemux::AutoRemux(QString inFile, QString outFile) +{ + if (inFile != "" && outFile != "" && autoRemux) { + ui->progressBar->setVisible(true); + emit remux(inFile, outFile); + autoRemuxFile = outFile; + } +} + +void OBSRemux::remuxNextEntry() +{ + worker->lastProgress = 0.f; + + QString inputPath, outputPath; + if (queueModel->beginNextEntry(inputPath, outputPath)) { + emit remux(inputPath, outputPath); + } else { + queueModel->autoRemux = autoRemux; + queueModel->endProcessing(); + + if (!autoRemux) { + OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), + queueModel->checkForErrors() ? QTStr("Remux.FinishedError") + : QTStr("Remux.Finished")); + } + + ui->progressBar->setVisible(autoRemux); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + setAcceptDrops(true); + } +} + +void OBSRemux::closeEvent(QCloseEvent *event) +{ + if (!stopRemux()) + event->ignore(); + else + QDialog::closeEvent(event); +} + +void OBSRemux::reject() +{ + if (!stopRemux()) + return; + + QDialog::reject(); +} + +void OBSRemux::updateProgress(float percent) +{ + ui->progressBar->setValue(percent * 10); +} + +void OBSRemux::remuxFinished(bool success) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + + queueModel->finishEntry(success); + + if (autoRemux && autoRemuxFile != "") { + QTimer::singleShot(3000, this, &OBSRemux::close); + + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); + } + + remuxNextEntry(); +} + +void OBSRemux::clearFinished() +{ + queueModel->clearFinished(); +} + +void OBSRemux::clearAll() +{ + queueModel->clearAll(); +} + +/********************************************************** + Worker thread - Executes the libobs remux operation as a + background process. +**********************************************************/ + +void RemuxWorker::UpdateProgress(float percent) +{ + if (abs(lastProgress - percent) < 0.1f) + return; + + emit updateProgress(percent); + lastProgress = percent; +} + +void RemuxWorker::remux(const QString &source, const QString &target) +{ + isWorking = true; + + auto callback = [](void *data, float percent) { + RemuxWorker *rw = static_cast(data); + + QMutexLocker lock(&rw->updateMutex); + + rw->UpdateProgress(percent); + + return rw->isWorking; + }; + + bool stopped = false; + bool success = false; + + media_remux_job_t mr_job = nullptr; + if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { + + success = media_remux_job_process(mr_job, callback, this); + + media_remux_job_destroy(mr_job); + + stopped = !isWorking; + } + + isWorking = false; + + emit remuxFinished(!stopped && success); +} diff --git a/frontend/utility/RemuxWorker.hpp b/frontend/utility/RemuxWorker.hpp new file mode 100644 index 000000000..20a13a780 --- /dev/null +++ b/frontend/utility/RemuxWorker.hpp @@ -0,0 +1,173 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + 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 +#include +#include +#include +#include +#include +#include "ui_OBSRemux.h" + +#include +#include + +class RemuxQueueModel; +class RemuxWorker; + +enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; +Q_DECLARE_METATYPE(RemuxEntryState); + +class OBSRemux : public QDialog { + Q_OBJECT + + QPointer queueModel; + QThread remuxer; + QPointer worker; + + std::unique_ptr ui; + + const char *recPath; + + virtual void closeEvent(QCloseEvent *event) override; + virtual void reject() override; + + bool autoRemux; + QString autoRemuxFile; + +public: + explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); + virtual ~OBSRemux() override; + + using job_t = std::shared_ptr; + + void AutoRemux(QString inFile, QString outFile); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + + void remuxNextEntry(); + +private slots: + void rowCountChanged(const QModelIndex &parent, int first, int last); + +public slots: + void updateProgress(float percent); + void remuxFinished(bool success); + void beginRemux(); + bool stopRemux(); + void clearFinished(); + void clearAll(); + +signals: + void remux(const QString &source, const QString &target); +}; + +class RemuxQueueModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSRemux; + +public: + RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + QFileInfoList checkForOverwrites() const; + bool checkForErrors() const; + void beginProcessing(); + void endProcessing(); + bool beginNextEntry(QString &inputPath, QString &outputPath); + void finishEntry(bool success); + bool canClearFinished() const; + void clearFinished(); + void clearAll(); + + bool autoRemux = false; + +private: + struct RemuxQueueEntry { + RemuxEntryState state; + + QString sourcePath; + QString targetPath; + }; + + QList queue; + bool isProcessing; + + static QVariant getIcon(RemuxEntryState state); + + void checkInputPath(int row); +}; + +class RemuxWorker : public QObject { + Q_OBJECT + + QMutex updateMutex; + + bool isWorking; + + float lastProgress; + void UpdateProgress(float percent); + + explicit RemuxWorker() : isWorking(false) {} + virtual ~RemuxWorker(){}; + +private slots: + void remux(const QString &source, const QString &target); + +signals: + void updateProgress(float percent); + void remuxFinished(bool success); + + friend class OBSRemux; +}; + +class RemuxEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; From 085c6245b0c36e4fa03527d36b4676ced5deef3e Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 18:36:57 +0100 Subject: [PATCH 06/37] frontend: Split Qt UI dialogs into single files per C++ class --- frontend/components/DelButton.hpp | 556 +---------- frontend/components/EditWidget.hpp | 555 +---------- frontend/dialogs/OBSBasicInteraction.cpp | 18 +- frontend/dialogs/OBSBasicInteraction.hpp | 28 +- frontend/dialogs/OBSBasicSourceSelect.cpp | 7 - frontend/dialogs/OBSBasicTransform.cpp | 5 - frontend/dialogs/OBSExtraBrowsers.cpp | 532 +---------- frontend/dialogs/OBSExtraBrowsers.hpp | 69 +- frontend/dialogs/OBSMissingFiles.cpp | 404 +------- frontend/dialogs/OBSMissingFiles.hpp | 69 +- frontend/dialogs/OBSRemux.cpp | 634 +------------ frontend/dialogs/OBSRemux.hpp | 105 +-- frontend/utility/ExtraBrowsersDelegate.cpp | 442 +-------- frontend/utility/ExtraBrowsersDelegate.hpp | 64 -- frontend/utility/ExtraBrowsersModel.cpp | 322 +------ frontend/utility/ExtraBrowsersModel.hpp | 49 +- frontend/utility/MissingFilesModel.cpp | 288 +----- frontend/utility/MissingFilesModel.hpp | 58 +- .../utility/MissingFilesPathItemDelegate.cpp | 388 +------- .../utility/MissingFilesPathItemDelegate.hpp | 69 -- frontend/utility/OBSEventFilter.hpp | 51 +- .../utility/RemuxEntryPathItemDelegate.cpp | 727 +-------------- .../utility/RemuxEntryPathItemDelegate.hpp | 127 --- frontend/utility/RemuxQueueModel.cpp | 554 +---------- frontend/utility/RemuxQueueModel.hpp | 115 +-- frontend/utility/RemuxWorker.cpp | 866 +----------------- frontend/utility/RemuxWorker.hpp | 131 +-- 27 files changed, 114 insertions(+), 7119 deletions(-) diff --git a/frontend/components/DelButton.hpp b/frontend/components/DelButton.hpp index 8f4232e1d..dab84e8b7 100644 --- a/frontend/components/DelButton.hpp +++ b/frontend/components/DelButton.hpp @@ -1,110 +1,6 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" +#pragma once -#include -#include -#include -#include - -#include - -#include "ui_OBSExtraBrowsers.h" - -using namespace json11; - -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} +#include class DelButton : public QPushButton { public: @@ -112,451 +8,3 @@ public: QPersistentModelIndex index; }; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/frontend/components/EditWidget.hpp b/frontend/components/EditWidget.hpp index 8f4232e1d..dbc431228 100644 --- a/frontend/components/EditWidget.hpp +++ b/frontend/components/EditWidget.hpp @@ -1,117 +1,9 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" +#pragma once -#include #include -#include -#include +#include -#include - -#include "ui_OBSExtraBrowsers.h" - -using namespace json11; - -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; +class QPersistentModelIndex; class EditWidget : public QLineEdit { public: @@ -119,444 +11,3 @@ public: QPersistentModelIndex index; }; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/frontend/dialogs/OBSBasicInteraction.cpp b/frontend/dialogs/OBSBasicInteraction.cpp index 93d572c5b..e4c2b705e 100644 --- a/frontend/dialogs/OBSBasicInteraction.cpp +++ b/frontend/dialogs/OBSBasicInteraction.cpp @@ -15,22 +15,24 @@ along with this program. If not, see . ******************************************************************************/ -#include "obs-app.hpp" -#include "moc_window-basic-interaction.cpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" +#include "OBSBasicInteraction.hpp" + +#include +#include +#include +#include #include -#include -#include -#include -#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif +#include "moc_OBSBasicInteraction.cpp" + using namespace std; OBSBasicInteraction::OBSBasicInteraction(QWidget *parent, OBSSource source_) diff --git a/frontend/dialogs/OBSBasicInteraction.hpp b/frontend/dialogs/OBSBasicInteraction.hpp index 6bfaaab4a..01e288b6b 100644 --- a/frontend/dialogs/OBSBasicInteraction.hpp +++ b/frontend/dialogs/OBSBasicInteraction.hpp @@ -17,17 +17,13 @@ #pragma once -#include -#include -#include - -#include -#include - -class OBSBasic; - #include "ui_OBSBasicInteraction.h" +#include + +#include + +class OBSBasic; class OBSEventFilter; class OBSBasicInteraction : public QDialog { @@ -66,17 +62,3 @@ protected: virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; }; - -typedef std::function EventFilterFunc; - -class OBSEventFilter : public QObject { - Q_OBJECT -public: - OBSEventFilter(EventFilterFunc filter_) : filter(filter_) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) { return filter(obj, event); } - -public: - EventFilterFunc filter; -}; diff --git a/frontend/dialogs/OBSBasicSourceSelect.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp index 84e9ad44e..7f7cc3a3c 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -332,13 +332,6 @@ static inline const char *GetSourceDisplayName(const char *id) return obs_source_get_display_name(v_id); } -Q_DECLARE_METATYPE(OBSScene); - -template static inline T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_, undo_stack &undo_s) : QDialog(parent), ui(new Ui::OBSBasicSourceSelect), diff --git a/frontend/dialogs/OBSBasicTransform.cpp b/frontend/dialogs/OBSBasicTransform.cpp index 173a68dc6..7889f84bc 100644 --- a/frontend/dialogs/OBSBasicTransform.cpp +++ b/frontend/dialogs/OBSBasicTransform.cpp @@ -351,11 +351,6 @@ void OBSBasicTransform::OnCropChanged() ignoreTransformSignal = false; } -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - void OBSBasicTransform::OnSceneChanged(QListWidgetItem *current, QListWidgetItem *) { if (!current) diff --git a/frontend/dialogs/OBSExtraBrowsers.cpp b/frontend/dialogs/OBSExtraBrowsers.cpp index 8f4232e1d..ab5789e79 100644 --- a/frontend/dialogs/OBSExtraBrowsers.cpp +++ b/frontend/dialogs/OBSExtraBrowsers.cpp @@ -1,413 +1,9 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" - -#include -#include -#include -#include - -#include - +#include "OBSExtraBrowsers.hpp" #include "ui_OBSExtraBrowsers.h" -using namespace json11; +#include -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ +#include "moc_OBSExtraBrowsers.cpp" OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) { @@ -438,125 +34,3 @@ void OBSExtraBrowsers::on_apply_clicked() { model->Apply(); } - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/frontend/dialogs/OBSExtraBrowsers.hpp b/frontend/dialogs/OBSExtraBrowsers.hpp index 690d784dd..c71a12cae 100644 --- a/frontend/dialogs/OBSExtraBrowsers.hpp +++ b/frontend/dialogs/OBSExtraBrowsers.hpp @@ -1,15 +1,10 @@ #pragma once +#include + #include -#include -#include -#include -#include class Ui_OBSExtraBrowsers; -class ExtraBrowsersModel; - -class QCefWidget; class OBSExtraBrowsers : public QDialog { Q_OBJECT @@ -26,63 +21,3 @@ public: public slots: void on_apply_clicked(); }; - -class ExtraBrowsersModel : public QAbstractTableModel { - Q_OBJECT - -public: - inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) - { - Reset(); - QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); - } - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - - struct Item { - int prevIdx; - QString title; - QString url; - }; - - void TabSelection(bool forward); - - void AddDeleteButton(int idx); - void Reset(); - void CheckToAdd(); - void UpdateItem(Item &item); - void DeleteItem(); - void Apply(); - - QVector items; - QVector deleted; - - QString newTitle; - QString newURL; - -public slots: - void Init(); -}; - -class ExtraBrowsersDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} - - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - - void setEditorData(QWidget *editor, const QModelIndex &index) const override; - - bool eventFilter(QObject *object, QEvent *event) override; - void RevertText(QLineEdit *edit); - bool UpdateText(QLineEdit *edit); - bool ValidName(const QString &text) const; - - ExtraBrowsersModel *model; -}; diff --git a/frontend/dialogs/OBSMissingFiles.cpp b/frontend/dialogs/OBSMissingFiles.cpp index 57e641a74..ca5d7a101 100644 --- a/frontend/dialogs/OBSMissingFiles.cpp +++ b/frontend/dialogs/OBSMissingFiles.cpp @@ -15,409 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-missing-files.cpp" -#include "window-basic-main.hpp" +#include "OBSMissingFiles.hpp" -#include "obs-app.hpp" +#include +#include +#include -#include -#include #include -#include - -enum MissingFilesColumn { - Source, - OriginalPath, - NewPath, - State, - - Count -}; +#include "moc_OBSMissingFiles.cpp" +// TODO: Fix redefinition error of due to clash with enums defined in importer code. enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &) const -{ - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in input cells - if (isOutput) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - - return container; -} - -void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - model->setData(index, list); - } else - model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text(), 0); - } -} - -void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) -{ - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = - QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); - -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } - - if (isSet) - emit commitData(container); -} - -void MissingFilesPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); - container->findChild()->clearFocus(); - ((QWidget *)container->parent())->setFocus(); - emit commitData(container); -} - -/** - Model -**/ - -MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) -{ - QStyle *style = QApplication::style(); - - warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); -} - -int MissingFilesModel::rowCount(const QModelIndex &) const -{ - return files.length(); -} - -int MissingFilesModel::columnCount(const QModelIndex &) const -{ - return MissingFilesColumn::Count; -} - -int MissingFilesModel::found() const -{ - int res = 0; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != Missing && files[i].state != Cleared) - res++; - } - - return res; -} - -QVariant MissingFilesModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= files.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - QFileInfo fi(files[index.row()].originalPath); - - switch (index.column()) { - case MissingFilesColumn::Source: - result = files[index.row()].source; - break; - case MissingFilesColumn::OriginalPath: - result = fi.fileName(); - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - case MissingFilesColumn::State: - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - } - break; - } - } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); - - if (source) { - result = main->GetSourceIcon(obs_source_get_id(source)); - } - } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { - QFont font = QFont(); - font.setBold(true); - - result = font; - } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - - default: - break; - } - } else if (role == Qt::ToolTipRole) { - switch (index.column()) { - case MissingFilesColumn::OriginalPath: - result = files[index.row()].originalPath; - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - default: - break; - } - } - - return result; -} - -Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == MissingFilesColumn::OriginalPath) { - flags &= ~Qt::ItemIsEditable; - } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) -{ - loop = false; - QUrl url = QUrl().fromLocalFile(path); - QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); - - bool prompted = skipPrompt; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != MissingFilesState::Missing) - continue; - - QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); - QString filename = origFile.fileName(); - QString testFile = dir + filename; - - if (os_file_exists(testFile.toStdString().c_str())) { - if (!prompted) { - QMessageBox::StandardButton button = - QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), - QTStr("MissingFiles.AutoSearchText")); - - if (button == QMessageBox::No) - break; - - prompted = true; - } - QModelIndex in = index(i, MissingFilesColumn::NewPath); - setData(in, testFile, 0); - } - } - loop = true; -} - -bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == MissingFilesRole::NewPathsToProcessRole) { - QStringList list = value.toStringList(); - - int row = index.row() + 1; - beginInsertRows(QModelIndex(), row, row); - - MissingFileEntry entry; - entry.originalPath = list[0].replace("\\", "/"); - entry.source = list[1]; - - files.insert(row, entry); - row++; - - endInsertRows(); - - success = true; - } else { - QString path = value.toString(); - if (index.column() == MissingFilesColumn::NewPath) { - files[index.row()].newPath = value.toString(); - QString fileName = QUrl(path).fileName(); - QString origFileName = QUrl(files[index.row()].originalPath).fileName(); - - if (path.isEmpty()) { - files[index.row()].state = MissingFilesState::Missing; - } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { - files[index.row()].state = MissingFilesState::Cleared; - } else if (fileName.compare(origFileName) == 0) { - files[index.row()].state = MissingFilesState::Found; - - if (loop) - fileCheckLoop(files, path, false); - } else { - files[index.row()].state = MissingFilesState::Replaced; - - if (loop) - fileCheckLoop(files, path, false); - } - - emit dataChanged(index, index); - success = true; - } - } - - return success; -} - -QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case MissingFilesColumn::State: - result = QTStr("MissingFiles.State"); - break; - case MissingFilesColumn::Source: - result = QTStr("Basic.Main.Source"); - break; - case MissingFilesColumn::OriginalPath: - result = QTStr("MissingFiles.MissingFile"); - break; - case MissingFilesColumn::NewPath: - result = QTStr("MissingFiles.NewFile"); - break; - } - } - - return result; -} +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesColumn { Source, OriginalPath, NewPath, State, Count }; OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) : QDialog(parent), diff --git a/frontend/dialogs/OBSMissingFiles.hpp b/frontend/dialogs/OBSMissingFiles.hpp index 2b24e0f8e..2d5bb41e6 100644 --- a/frontend/dialogs/OBSMissingFiles.hpp +++ b/frontend/dialogs/OBSMissingFiles.hpp @@ -17,15 +17,14 @@ #pragma once -#include -#include -#include "obs-app.hpp" #include "ui_OBSMissingFiles.h" -class MissingFilesModel; +#include -enum MissingFilesState { Missing, Found, Replaced, Cleared }; -Q_DECLARE_METATYPE(MissingFilesState); +#include +#include + +class MissingFilesModel; class OBSMissingFiles : public QDialog { Q_OBJECT @@ -52,61 +51,3 @@ private: public slots: void dataChanged(); }; - -class MissingFilesModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSMissingFiles; - -public: - explicit MissingFilesModel(QObject *parent = 0); - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - int found() const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - bool loop = true; - - QIcon warningIcon; - -private: - struct MissingFileEntry { - MissingFilesState state = MissingFilesState::Missing; - - QString source; - - QString originalPath; - QString newPath; - }; - - QList files; - - void fileCheckLoop(QList files, QString path, bool skipPrompt); -}; - -class MissingFilesPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); -}; diff --git a/frontend/dialogs/OBSRemux.cpp b/frontend/dialogs/OBSRemux.cpp index d72ebbd9c..84b28372e 100644 --- a/frontend/dialogs/OBSRemux.cpp +++ b/frontend/dialogs/OBSRemux.cpp @@ -15,589 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-remux.cpp" +#include "OBSRemux.hpp" -#include "obs-app.hpp" +#include +#include +#include +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include "window-basic-main.hpp" +#include +#include +#include +#include -#include -#include - -using namespace std; - -enum RemuxEntryColumn { - State, - InputPath, - OutputPath, - - Count -}; - -enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { - // Never allow modification of rows that are - // in progress. - return Q_NULLPTR; - } else if (isOutput && state != RemuxEntryState::Ready) { - // Do not allow modification of output rows - // that aren't associated with a valid input. - return Q_NULLPTR; - } else if (!isOutput && state == RemuxEntryState::Complete) { - // Don't allow modification of rows that are - // already complete. - return Q_NULLPTR; - } else { - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!isOutput && state != RemuxEntryState::Empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; - } -} - -void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - if (list.size() > 0) - model->setData(index, list); - } else - model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - if (isOutput) { - if (state != Ready) { - QColor background = - localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); - - localOption.backgroundBrush = QBrush(background); - } - } - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty()) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } else { - QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, - QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - } - - if (isSet) - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/********************************************************** - Model - Manages the queue's data -**********************************************************/ - -int RemuxQueueModel::rowCount(const QModelIndex &) const -{ - return queue.length() + (isProcessing ? 0 : 1); -} - -int RemuxQueueModel::columnCount(const QModelIndex &) const -{ - return RemuxEntryColumn::Count; -} - -QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= queue.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - result = queue[index.row()].sourcePath; - break; - case RemuxEntryColumn::OutputPath: - result = queue[index.row()].targetPath; - break; - } - } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { - result = getIcon(queue[index.row()].state); - } else if (role == RemuxEntryRole::EntryStateRole) { - result = queue[index.row()].state; - } - - return result; -} - -QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case RemuxEntryColumn::State: - result = QString(); - break; - case RemuxEntryColumn::InputPath: - result = QTStr("Remux.SourceFile"); - break; - case RemuxEntryColumn::OutputPath: - result = QTStr("Remux.TargetFile"); - break; - } - } - - return result; -} - -Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == RemuxEntryColumn::InputPath) { - flags |= Qt::ItemIsEditable; - } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == RemuxEntryRole::NewPathsToProcessRole) { - QStringList pathList = value.toStringList(); - - if (pathList.size() == 0) { - if (index.row() < queue.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (pathList.size() >= 1 && index.row() < queue.length()) { - queue[index.row()].sourcePath = pathList[0]; - checkInputPath(index.row()); - - pathList.removeAt(0); - - success = true; - } - - if (pathList.size() > 0) { - int row = index.row(); - int lastRow = row + pathList.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : pathList) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - queue.insert(row, entry); - row++; - } - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - - success = true; - } - } - } else if (index.row() == queue.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); - queue.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - success = true; - } - } else { - QString path = value.toString(); - - if (path.isEmpty()) { - if (index.column() == RemuxEntryColumn::InputPath) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - queue[index.row()].sourcePath = value.toString(); - checkInputPath(index.row()); - success = true; - break; - case RemuxEntryColumn::OutputPath: - queue[index.row()].targetPath = value.toString(); - emit dataChanged(index, index); - success = true; - break; - } - } - } - - return success; -} - -QVariant RemuxQueueModel::getIcon(RemuxEntryState state) -{ - QVariant icon; - QStyle *style = QApplication::style(); - - switch (state) { - case RemuxEntryState::Complete: - icon = style->standardIcon(QStyle::SP_DialogApplyButton); - break; - - case RemuxEntryState::InProgress: - icon = style->standardIcon(QStyle::SP_ArrowRight); - break; - - case RemuxEntryState::Error: - icon = style->standardIcon(QStyle::SP_DialogCancelButton); - break; - - case RemuxEntryState::InvalidPath: - icon = style->standardIcon(QStyle::SP_MessageBoxWarning); - break; - - default: - break; - } - - return icon; -} - -void RemuxQueueModel::checkInputPath(int row) -{ - RemuxQueueEntry &entry = queue[row]; - - if (entry.sourcePath.isEmpty()) { - entry.state = RemuxEntryState::Empty; - } else { - entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); - QFileInfo fileInfo(entry.sourcePath); - if (fileInfo.exists()) - entry.state = RemuxEntryState::Ready; - else - entry.state = RemuxEntryState::InvalidPath; - - QString newExt = ".mp4"; - QString suffix = fileInfo.suffix(); - - if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { - newExt = ".remuxed." + suffix; - } - - if (entry.state == RemuxEntryState::Ready) - entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + - fileInfo.completeBaseName() + newExt); - } - - if (entry.state == RemuxEntryState::Ready && isProcessing) - entry.state = RemuxEntryState::Pending; - - emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); -} - -QFileInfoList RemuxQueueModel::checkForOverwrites() const -{ - QFileInfoList list; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Ready) { - QFileInfo fileInfo(entry.targetPath); - if (fileInfo.exists()) { - list.append(fileInfo); - } - } - } - - return list; -} - -bool RemuxQueueModel::checkForErrors() const -{ - bool hasErrors = false; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Error) { - hasErrors = true; - break; - } - } - - return hasErrors; -} - -void RemuxQueueModel::clearAll() -{ - beginRemoveRows(QModelIndex(), 0, queue.size() - 1); - queue.clear(); - endRemoveRows(); -} - -void RemuxQueueModel::clearFinished() -{ - int index = 0; - - for (index = 0; index < queue.size(); index++) { - const RemuxQueueEntry &entry = queue[index]; - if (entry.state == RemuxEntryState::Complete) { - beginRemoveRows(QModelIndex(), index, index); - queue.removeAt(index); - endRemoveRows(); - index--; - } - } -} - -bool RemuxQueueModel::canClearFinished() const -{ - bool canClearFinished = false; - for (const RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Complete) { - canClearFinished = true; - break; - } - - return canClearFinished; -} - -void RemuxQueueModel::beginProcessing() -{ - for (RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Ready) - entry.state = RemuxEntryState::Pending; - - // Signal that the insertion point no longer exists. - beginRemoveRows(QModelIndex(), queue.length(), queue.length()); - endRemoveRows(); - - isProcessing = true; - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -void RemuxQueueModel::endProcessing() -{ - for (RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::Ready; - } - } - - // Signal that the insertion point exists again. - isProcessing = false; - if (!autoRemux) { - beginInsertRows(QModelIndex(), queue.length(), queue.length()); - endInsertRows(); - } - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) -{ - bool anyStarted = false; - - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::InProgress; - - inputPath = entry.sourcePath; - outputPath = entry.targetPath; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - anyStarted = true; - break; - } - } - - return anyStarted; -} - -void RemuxQueueModel::finishEntry(bool success) -{ - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::InProgress) { - if (success) - entry.state = RemuxEntryState::Complete; - else - entry.state = RemuxEntryState::Error; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - break; - } - } -} - -/********************************************************** - The actual remux window implementation -**********************************************************/ +#include "moc_OBSRemux.cpp" OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) : QDialog(parent), @@ -876,49 +308,3 @@ void OBSRemux::clearAll() { queueModel->clearAll(); } - -/********************************************************** - Worker thread - Executes the libobs remux operation as a - background process. -**********************************************************/ - -void RemuxWorker::UpdateProgress(float percent) -{ - if (abs(lastProgress - percent) < 0.1f) - return; - - emit updateProgress(percent); - lastProgress = percent; -} - -void RemuxWorker::remux(const QString &source, const QString &target) -{ - isWorking = true; - - auto callback = [](void *data, float percent) { - RemuxWorker *rw = static_cast(data); - - QMutexLocker lock(&rw->updateMutex); - - rw->UpdateProgress(percent); - - return rw->isWorking; - }; - - bool stopped = false; - bool success = false; - - media_remux_job_t mr_job = nullptr; - if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { - - success = media_remux_job_process(mr_job, callback, this); - - media_remux_job_destroy(mr_job); - - stopped = !isWorking; - } - - isWorking = false; - - emit remuxFinished(!stopped && success); -} diff --git a/frontend/dialogs/OBSRemux.hpp b/frontend/dialogs/OBSRemux.hpp index 20a13a780..87d68587e 100644 --- a/frontend/dialogs/OBSRemux.hpp +++ b/frontend/dialogs/OBSRemux.hpp @@ -17,23 +17,14 @@ #pragma once -#include -#include -#include -#include -#include -#include #include "ui_OBSRemux.h" -#include -#include +#include +#include class RemuxQueueModel; class RemuxWorker; -enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; -Q_DECLARE_METATYPE(RemuxEntryState); - class OBSRemux : public QDialog { Q_OBJECT @@ -79,95 +70,3 @@ public slots: signals: void remux(const QString &source, const QString &target); }; - -class RemuxQueueModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSRemux; - -public: - RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - QFileInfoList checkForOverwrites() const; - bool checkForErrors() const; - void beginProcessing(); - void endProcessing(); - bool beginNextEntry(QString &inputPath, QString &outputPath); - void finishEntry(bool success); - bool canClearFinished() const; - void clearFinished(); - void clearAll(); - - bool autoRemux = false; - -private: - struct RemuxQueueEntry { - RemuxEntryState state; - - QString sourcePath; - QString targetPath; - }; - - QList queue; - bool isProcessing; - - static QVariant getIcon(RemuxEntryState state); - - void checkInputPath(int row); -}; - -class RemuxWorker : public QObject { - Q_OBJECT - - QMutex updateMutex; - - bool isWorking; - - float lastProgress; - void UpdateProgress(float percent); - - explicit RemuxWorker() : isWorking(false) {} - virtual ~RemuxWorker(){}; - -private slots: - void remux(const QString &source, const QString &target); - -signals: - void updateProgress(float percent); - void remuxFinished(bool success); - - friend class OBSRemux; -}; - -class RemuxEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; diff --git a/frontend/utility/ExtraBrowsersDelegate.cpp b/frontend/utility/ExtraBrowsersDelegate.cpp index 8f4232e1d..e984797fb 100644 --- a/frontend/utility/ExtraBrowsersDelegate.cpp +++ b/frontend/utility/ExtraBrowsersDelegate.cpp @@ -1,289 +1,13 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" +#include "ExtraBrowsersDelegate.hpp" +#include "ExtraBrowsersModel.hpp" + +#include #include -#include -#include -#include -#include +#include -#include "ui_OBSExtraBrowsers.h" - -using namespace json11; - -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ +#include "moc_ExtraBrowsersDelegate.cpp" QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const @@ -406,157 +130,3 @@ bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) emit commitData(edit); return true; } - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/frontend/utility/ExtraBrowsersDelegate.hpp b/frontend/utility/ExtraBrowsersDelegate.hpp index 690d784dd..6abafc153 100644 --- a/frontend/utility/ExtraBrowsersDelegate.hpp +++ b/frontend/utility/ExtraBrowsersDelegate.hpp @@ -1,73 +1,9 @@ #pragma once -#include -#include -#include #include -#include -class Ui_OBSExtraBrowsers; class ExtraBrowsersModel; -class QCefWidget; - -class OBSExtraBrowsers : public QDialog { - Q_OBJECT - - std::unique_ptr ui; - ExtraBrowsersModel *model; - -public: - OBSExtraBrowsers(QWidget *parent); - ~OBSExtraBrowsers(); - - void closeEvent(QCloseEvent *event) override; - -public slots: - void on_apply_clicked(); -}; - -class ExtraBrowsersModel : public QAbstractTableModel { - Q_OBJECT - -public: - inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) - { - Reset(); - QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); - } - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - - struct Item { - int prevIdx; - QString title; - QString url; - }; - - void TabSelection(bool forward); - - void AddDeleteButton(int idx); - void Reset(); - void CheckToAdd(); - void UpdateItem(Item &item); - void DeleteItem(); - void Apply(); - - QVector items; - QVector deleted; - - QString newTitle; - QString newURL; - -public slots: - void Init(); -}; - class ExtraBrowsersDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/utility/ExtraBrowsersModel.cpp b/frontend/utility/ExtraBrowsersModel.cpp index 8f4232e1d..5409c3c19 100644 --- a/frontend/utility/ExtraBrowsersModel.cpp +++ b/frontend/utility/ExtraBrowsersModel.cpp @@ -1,29 +1,14 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" +#include "ExtraBrowsersModel.hpp" + +#include +#include +#include #include -#include -#include -#include -#include +#include -#include "ui_OBSExtraBrowsers.h" - -using namespace json11; - -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ +#include "moc_ExtraBrowsersModel.cpp" void ExtraBrowsersModel::Reset() { @@ -105,21 +90,6 @@ Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const return flags; } - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - void ExtraBrowsersModel::AddDeleteButton(int idx) { QTableView *widget = reinterpret_cast(parent()); @@ -282,281 +252,3 @@ void ExtraBrowsersModel::Init() for (int i = 0; i < items.count(); i++) AddDeleteButton(i); } - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/frontend/utility/ExtraBrowsersModel.hpp b/frontend/utility/ExtraBrowsersModel.hpp index 690d784dd..2424875e6 100644 --- a/frontend/utility/ExtraBrowsersModel.hpp +++ b/frontend/utility/ExtraBrowsersModel.hpp @@ -1,32 +1,18 @@ #pragma once -#include -#include #include -#include -#include +#include -class Ui_OBSExtraBrowsers; -class ExtraBrowsersModel; +enum class Column : int { + Title, + Url, + Delete, -class QCefWidget; - -class OBSExtraBrowsers : public QDialog { - Q_OBJECT - - std::unique_ptr ui; - ExtraBrowsersModel *model; - -public: - OBSExtraBrowsers(QWidget *parent); - ~OBSExtraBrowsers(); - - void closeEvent(QCloseEvent *event) override; - -public slots: - void on_apply_clicked(); + Count, }; +#define OBJ_NAME_SUFFIX "_extraBrowser" + class ExtraBrowsersModel : public QAbstractTableModel { Q_OBJECT @@ -67,22 +53,3 @@ public: public slots: void Init(); }; - -class ExtraBrowsersDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} - - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - - void setEditorData(QWidget *editor, const QModelIndex &index) const override; - - bool eventFilter(QObject *object, QEvent *event) override; - void RevertText(QLineEdit *edit); - bool UpdateText(QLineEdit *edit); - bool ValidName(const QString &text) const; - - ExtraBrowsersModel *model; -}; diff --git a/frontend/utility/MissingFilesModel.cpp b/frontend/utility/MissingFilesModel.cpp index 57e641a74..bda79680a 100644 --- a/frontend/utility/MissingFilesModel.cpp +++ b/frontend/utility/MissingFilesModel.cpp @@ -15,170 +15,20 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-missing-files.cpp" -#include "window-basic-main.hpp" +#include "MissingFilesModel.hpp" -#include "obs-app.hpp" +#include -#include -#include -#include +#include +#include -#include - -enum MissingFilesColumn { - Source, - OriginalPath, - NewPath, - State, - - Count -}; +#include "moc_MissingFilesModel.cpp" +// TODO: Fix redefinition error of due to clash with enums defined in importer code. enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &) const -{ - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in input cells - if (isOutput) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - - return container; -} - -void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - model->setData(index, list); - } else - model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text(), 0); - } -} - -void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) -{ - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = - QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); - -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } - - if (isSet) - emit commitData(container); -} - -void MissingFilesPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); - container->findChild()->clearFocus(); - ((QWidget *)container->parent())->setFocus(); - emit commitData(container); -} - -/** - Model -**/ +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesColumn { Source, OriginalPath, NewPath, State, Count }; MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) { @@ -418,125 +268,3 @@ QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, return result; } - -OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) - : QDialog(parent), - filesModel(new MissingFilesModel), - ui(new Ui::OBSMissingFiles) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(filesModel); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, - new MissingFilesPathItemDelegate(false, "")); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, - new MissingFilesPathItemDelegate(true, "")); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); - - for (size_t i = 0; i < obs_missing_files_count(files); i++) { - obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); - - const char *oldPath = obs_missing_file_get_path(f); - const char *name = obs_missing_file_get_source_name(f); - - addMissingFile(oldPath, name); - } - - QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); - - ui->found->setText(found); - - fileStore = files; - - connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); - connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); - connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); - connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); - - QModelIndex index = filesModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -OBSMissingFiles::~OBSMissingFiles() -{ - obs_missing_files_destroy(fileStore); -} - -void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) -{ - QStringList list; - - list.append(originalPath); - list.append(sourceName); - - QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); - - filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); -} - -void OBSMissingFiles::saveFiles() -{ - for (int i = 0; i < filesModel->files.length(); i++) { - MissingFilesState state = filesModel->files[i].state; - if (state != MissingFilesState::Missing) { - obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); - - QString path = filesModel->files[i].newPath; - - if (state == MissingFilesState::Cleared) { - obs_missing_file_issue_callback(f, ""); - } else { - char *p = bstrdup(path.toStdString().c_str()); - obs_missing_file_issue_callback(f, p); - bfree(p); - } - } - } - - QDialog::accept(); -} - -void OBSMissingFiles::browseFolders() -{ - QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if (dir != "") { - dir += "/"; - filesModel->fileCheckLoop(filesModel->files, dir, true); - } -} - -void OBSMissingFiles::dataChanged() -{ - QString found = - QTStr("MissingFiles.NumFound") - .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); - - ui->found->setText(found); - - ui->tableView->resizeColumnToContents(MissingFilesColumn::State); - ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); -} - -QIcon OBSMissingFiles::GetWarningIcon() -{ - return filesModel->warningIcon; -} - -void OBSMissingFiles::SetWarningIcon(const QIcon &icon) -{ - ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); - filesModel->warningIcon = icon; -} diff --git a/frontend/utility/MissingFilesModel.hpp b/frontend/utility/MissingFilesModel.hpp index 2b24e0f8e..4b7057e84 100644 --- a/frontend/utility/MissingFilesModel.hpp +++ b/frontend/utility/MissingFilesModel.hpp @@ -17,42 +17,13 @@ #pragma once -#include -#include -#include "obs-app.hpp" -#include "ui_OBSMissingFiles.h" - -class MissingFilesModel; +#include +#include enum MissingFilesState { Missing, Found, Replaced, Cleared }; + Q_DECLARE_METATYPE(MissingFilesState); -class OBSMissingFiles : public QDialog { - Q_OBJECT - Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) - - QPointer filesModel; - std::unique_ptr ui; - -public: - explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); - virtual ~OBSMissingFiles() override; - - void addMissingFile(const char *originalPath, const char *sourceName); - - QIcon GetWarningIcon(); - void SetWarningIcon(const QIcon &icon); - -private: - void saveFiles(); - void browseFolders(); - - obs_missing_files_t *fileStore; - -public slots: - void dataChanged(); -}; - class MissingFilesModel : public QAbstractTableModel { Q_OBJECT @@ -87,26 +58,3 @@ private: void fileCheckLoop(QList files, QString path, bool skipPrompt); }; - -class MissingFilesPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); -}; diff --git a/frontend/utility/MissingFilesPathItemDelegate.cpp b/frontend/utility/MissingFilesPathItemDelegate.cpp index 57e641a74..f1e722511 100644 --- a/frontend/utility/MissingFilesPathItemDelegate.cpp +++ b/frontend/utility/MissingFilesPathItemDelegate.cpp @@ -15,32 +15,19 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-missing-files.cpp" -#include "window-basic-main.hpp" +#include "MissingFilesPathItemDelegate.hpp" -#include "obs-app.hpp" +#include +#include +#include #include #include -#include -#include - -enum MissingFilesColumn { - Source, - OriginalPath, - NewPath, - State, - - Count -}; +#include "moc_MissingFilesPathItemDelegate.cpp" enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) : QStyledItemDelegate(), isOutput(isOutput), @@ -175,368 +162,3 @@ void MissingFilesPathItemDelegate::handleClear(QWidget *container) ((QWidget *)container->parent())->setFocus(); emit commitData(container); } - -/** - Model -**/ - -MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) -{ - QStyle *style = QApplication::style(); - - warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); -} - -int MissingFilesModel::rowCount(const QModelIndex &) const -{ - return files.length(); -} - -int MissingFilesModel::columnCount(const QModelIndex &) const -{ - return MissingFilesColumn::Count; -} - -int MissingFilesModel::found() const -{ - int res = 0; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != Missing && files[i].state != Cleared) - res++; - } - - return res; -} - -QVariant MissingFilesModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= files.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - QFileInfo fi(files[index.row()].originalPath); - - switch (index.column()) { - case MissingFilesColumn::Source: - result = files[index.row()].source; - break; - case MissingFilesColumn::OriginalPath: - result = fi.fileName(); - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - case MissingFilesColumn::State: - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - } - break; - } - } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); - - if (source) { - result = main->GetSourceIcon(obs_source_get_id(source)); - } - } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { - QFont font = QFont(); - font.setBold(true); - - result = font; - } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - - default: - break; - } - } else if (role == Qt::ToolTipRole) { - switch (index.column()) { - case MissingFilesColumn::OriginalPath: - result = files[index.row()].originalPath; - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - default: - break; - } - } - - return result; -} - -Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == MissingFilesColumn::OriginalPath) { - flags &= ~Qt::ItemIsEditable; - } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) -{ - loop = false; - QUrl url = QUrl().fromLocalFile(path); - QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); - - bool prompted = skipPrompt; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != MissingFilesState::Missing) - continue; - - QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); - QString filename = origFile.fileName(); - QString testFile = dir + filename; - - if (os_file_exists(testFile.toStdString().c_str())) { - if (!prompted) { - QMessageBox::StandardButton button = - QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), - QTStr("MissingFiles.AutoSearchText")); - - if (button == QMessageBox::No) - break; - - prompted = true; - } - QModelIndex in = index(i, MissingFilesColumn::NewPath); - setData(in, testFile, 0); - } - } - loop = true; -} - -bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == MissingFilesRole::NewPathsToProcessRole) { - QStringList list = value.toStringList(); - - int row = index.row() + 1; - beginInsertRows(QModelIndex(), row, row); - - MissingFileEntry entry; - entry.originalPath = list[0].replace("\\", "/"); - entry.source = list[1]; - - files.insert(row, entry); - row++; - - endInsertRows(); - - success = true; - } else { - QString path = value.toString(); - if (index.column() == MissingFilesColumn::NewPath) { - files[index.row()].newPath = value.toString(); - QString fileName = QUrl(path).fileName(); - QString origFileName = QUrl(files[index.row()].originalPath).fileName(); - - if (path.isEmpty()) { - files[index.row()].state = MissingFilesState::Missing; - } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { - files[index.row()].state = MissingFilesState::Cleared; - } else if (fileName.compare(origFileName) == 0) { - files[index.row()].state = MissingFilesState::Found; - - if (loop) - fileCheckLoop(files, path, false); - } else { - files[index.row()].state = MissingFilesState::Replaced; - - if (loop) - fileCheckLoop(files, path, false); - } - - emit dataChanged(index, index); - success = true; - } - } - - return success; -} - -QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case MissingFilesColumn::State: - result = QTStr("MissingFiles.State"); - break; - case MissingFilesColumn::Source: - result = QTStr("Basic.Main.Source"); - break; - case MissingFilesColumn::OriginalPath: - result = QTStr("MissingFiles.MissingFile"); - break; - case MissingFilesColumn::NewPath: - result = QTStr("MissingFiles.NewFile"); - break; - } - } - - return result; -} - -OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) - : QDialog(parent), - filesModel(new MissingFilesModel), - ui(new Ui::OBSMissingFiles) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(filesModel); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, - new MissingFilesPathItemDelegate(false, "")); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, - new MissingFilesPathItemDelegate(true, "")); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); - - for (size_t i = 0; i < obs_missing_files_count(files); i++) { - obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); - - const char *oldPath = obs_missing_file_get_path(f); - const char *name = obs_missing_file_get_source_name(f); - - addMissingFile(oldPath, name); - } - - QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); - - ui->found->setText(found); - - fileStore = files; - - connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); - connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); - connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); - connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); - - QModelIndex index = filesModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -OBSMissingFiles::~OBSMissingFiles() -{ - obs_missing_files_destroy(fileStore); -} - -void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) -{ - QStringList list; - - list.append(originalPath); - list.append(sourceName); - - QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); - - filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); -} - -void OBSMissingFiles::saveFiles() -{ - for (int i = 0; i < filesModel->files.length(); i++) { - MissingFilesState state = filesModel->files[i].state; - if (state != MissingFilesState::Missing) { - obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); - - QString path = filesModel->files[i].newPath; - - if (state == MissingFilesState::Cleared) { - obs_missing_file_issue_callback(f, ""); - } else { - char *p = bstrdup(path.toStdString().c_str()); - obs_missing_file_issue_callback(f, p); - bfree(p); - } - } - } - - QDialog::accept(); -} - -void OBSMissingFiles::browseFolders() -{ - QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if (dir != "") { - dir += "/"; - filesModel->fileCheckLoop(filesModel->files, dir, true); - } -} - -void OBSMissingFiles::dataChanged() -{ - QString found = - QTStr("MissingFiles.NumFound") - .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); - - ui->found->setText(found); - - ui->tableView->resizeColumnToContents(MissingFilesColumn::State); - ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); -} - -QIcon OBSMissingFiles::GetWarningIcon() -{ - return filesModel->warningIcon; -} - -void OBSMissingFiles::SetWarningIcon(const QIcon &icon) -{ - ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); - filesModel->warningIcon = icon; -} diff --git a/frontend/utility/MissingFilesPathItemDelegate.hpp b/frontend/utility/MissingFilesPathItemDelegate.hpp index 2b24e0f8e..8b9606226 100644 --- a/frontend/utility/MissingFilesPathItemDelegate.hpp +++ b/frontend/utility/MissingFilesPathItemDelegate.hpp @@ -17,76 +17,7 @@ #pragma once -#include #include -#include "obs-app.hpp" -#include "ui_OBSMissingFiles.h" - -class MissingFilesModel; - -enum MissingFilesState { Missing, Found, Replaced, Cleared }; -Q_DECLARE_METATYPE(MissingFilesState); - -class OBSMissingFiles : public QDialog { - Q_OBJECT - Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) - - QPointer filesModel; - std::unique_ptr ui; - -public: - explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); - virtual ~OBSMissingFiles() override; - - void addMissingFile(const char *originalPath, const char *sourceName); - - QIcon GetWarningIcon(); - void SetWarningIcon(const QIcon &icon); - -private: - void saveFiles(); - void browseFolders(); - - obs_missing_files_t *fileStore; - -public slots: - void dataChanged(); -}; - -class MissingFilesModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSMissingFiles; - -public: - explicit MissingFilesModel(QObject *parent = 0); - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - int found() const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - bool loop = true; - - QIcon warningIcon; - -private: - struct MissingFileEntry { - MissingFilesState state = MissingFilesState::Missing; - - QString source; - - QString originalPath; - QString newPath; - }; - - QList files; - - void fileCheckLoop(QList files, QString path, bool skipPrompt); -}; class MissingFilesPathItemDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/utility/OBSEventFilter.hpp b/frontend/utility/OBSEventFilter.hpp index 6bfaaab4a..fe83ea674 100644 --- a/frontend/utility/OBSEventFilter.hpp +++ b/frontend/utility/OBSEventFilter.hpp @@ -17,55 +17,8 @@ #pragma once -#include -#include -#include - -#include -#include - -class OBSBasic; - -#include "ui_OBSBasicInteraction.h" - -class OBSEventFilter; - -class OBSBasicInteraction : public QDialog { - Q_OBJECT - -private: - OBSBasic *main; - - std::unique_ptr ui; - OBSSource source; - OBSSignal removedSignal; - OBSSignal renamedSignal; - std::unique_ptr eventFilter; - - static void SourceRemoved(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void DrawPreview(void *data, uint32_t cx, uint32_t cy); - - bool GetSourceRelativeXY(int mouseX, int mouseY, int &x, int &y); - - bool HandleMouseClickEvent(QMouseEvent *event); - bool HandleMouseMoveEvent(QMouseEvent *event); - bool HandleMouseWheelEvent(QWheelEvent *event); - bool HandleFocusEvent(QFocusEvent *event); - bool HandleKeyEvent(QKeyEvent *event); - - OBSEventFilter *BuildEventFilter(); - -public: - OBSBasicInteraction(QWidget *parent, OBSSource source_); - ~OBSBasicInteraction(); - - void Init(); - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; -}; +#include +#include typedef std::function EventFilterFunc; diff --git a/frontend/utility/RemuxEntryPathItemDelegate.cpp b/frontend/utility/RemuxEntryPathItemDelegate.cpp index d72ebbd9c..0349d1fab 100644 --- a/frontend/utility/RemuxEntryPathItemDelegate.cpp +++ b/frontend/utility/RemuxEntryPathItemDelegate.cpp @@ -15,44 +15,18 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-remux.cpp" +#include "RemuxEntryPathItemDelegate.hpp" +#include "RemuxQueueModel.hpp" -#include "obs-app.hpp" +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include "window-basic-main.hpp" +#include +#include +#include -#include -#include - -using namespace std; - -enum RemuxEntryColumn { - State, - InputPath, - OutputPath, - - Count -}; - -enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ +#include "moc_RemuxEntryPathItemDelegate.cpp" RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) : QStyledItemDelegate(), @@ -235,690 +209,3 @@ void RemuxEntryPathItemDelegate::updateText() QWidget *editor = lineEdit->parentWidget(); emit commitData(editor); } - -/********************************************************** - Model - Manages the queue's data -**********************************************************/ - -int RemuxQueueModel::rowCount(const QModelIndex &) const -{ - return queue.length() + (isProcessing ? 0 : 1); -} - -int RemuxQueueModel::columnCount(const QModelIndex &) const -{ - return RemuxEntryColumn::Count; -} - -QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= queue.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - result = queue[index.row()].sourcePath; - break; - case RemuxEntryColumn::OutputPath: - result = queue[index.row()].targetPath; - break; - } - } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { - result = getIcon(queue[index.row()].state); - } else if (role == RemuxEntryRole::EntryStateRole) { - result = queue[index.row()].state; - } - - return result; -} - -QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case RemuxEntryColumn::State: - result = QString(); - break; - case RemuxEntryColumn::InputPath: - result = QTStr("Remux.SourceFile"); - break; - case RemuxEntryColumn::OutputPath: - result = QTStr("Remux.TargetFile"); - break; - } - } - - return result; -} - -Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == RemuxEntryColumn::InputPath) { - flags |= Qt::ItemIsEditable; - } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == RemuxEntryRole::NewPathsToProcessRole) { - QStringList pathList = value.toStringList(); - - if (pathList.size() == 0) { - if (index.row() < queue.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (pathList.size() >= 1 && index.row() < queue.length()) { - queue[index.row()].sourcePath = pathList[0]; - checkInputPath(index.row()); - - pathList.removeAt(0); - - success = true; - } - - if (pathList.size() > 0) { - int row = index.row(); - int lastRow = row + pathList.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : pathList) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - queue.insert(row, entry); - row++; - } - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - - success = true; - } - } - } else if (index.row() == queue.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); - queue.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - success = true; - } - } else { - QString path = value.toString(); - - if (path.isEmpty()) { - if (index.column() == RemuxEntryColumn::InputPath) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - queue[index.row()].sourcePath = value.toString(); - checkInputPath(index.row()); - success = true; - break; - case RemuxEntryColumn::OutputPath: - queue[index.row()].targetPath = value.toString(); - emit dataChanged(index, index); - success = true; - break; - } - } - } - - return success; -} - -QVariant RemuxQueueModel::getIcon(RemuxEntryState state) -{ - QVariant icon; - QStyle *style = QApplication::style(); - - switch (state) { - case RemuxEntryState::Complete: - icon = style->standardIcon(QStyle::SP_DialogApplyButton); - break; - - case RemuxEntryState::InProgress: - icon = style->standardIcon(QStyle::SP_ArrowRight); - break; - - case RemuxEntryState::Error: - icon = style->standardIcon(QStyle::SP_DialogCancelButton); - break; - - case RemuxEntryState::InvalidPath: - icon = style->standardIcon(QStyle::SP_MessageBoxWarning); - break; - - default: - break; - } - - return icon; -} - -void RemuxQueueModel::checkInputPath(int row) -{ - RemuxQueueEntry &entry = queue[row]; - - if (entry.sourcePath.isEmpty()) { - entry.state = RemuxEntryState::Empty; - } else { - entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); - QFileInfo fileInfo(entry.sourcePath); - if (fileInfo.exists()) - entry.state = RemuxEntryState::Ready; - else - entry.state = RemuxEntryState::InvalidPath; - - QString newExt = ".mp4"; - QString suffix = fileInfo.suffix(); - - if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { - newExt = ".remuxed." + suffix; - } - - if (entry.state == RemuxEntryState::Ready) - entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + - fileInfo.completeBaseName() + newExt); - } - - if (entry.state == RemuxEntryState::Ready && isProcessing) - entry.state = RemuxEntryState::Pending; - - emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); -} - -QFileInfoList RemuxQueueModel::checkForOverwrites() const -{ - QFileInfoList list; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Ready) { - QFileInfo fileInfo(entry.targetPath); - if (fileInfo.exists()) { - list.append(fileInfo); - } - } - } - - return list; -} - -bool RemuxQueueModel::checkForErrors() const -{ - bool hasErrors = false; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Error) { - hasErrors = true; - break; - } - } - - return hasErrors; -} - -void RemuxQueueModel::clearAll() -{ - beginRemoveRows(QModelIndex(), 0, queue.size() - 1); - queue.clear(); - endRemoveRows(); -} - -void RemuxQueueModel::clearFinished() -{ - int index = 0; - - for (index = 0; index < queue.size(); index++) { - const RemuxQueueEntry &entry = queue[index]; - if (entry.state == RemuxEntryState::Complete) { - beginRemoveRows(QModelIndex(), index, index); - queue.removeAt(index); - endRemoveRows(); - index--; - } - } -} - -bool RemuxQueueModel::canClearFinished() const -{ - bool canClearFinished = false; - for (const RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Complete) { - canClearFinished = true; - break; - } - - return canClearFinished; -} - -void RemuxQueueModel::beginProcessing() -{ - for (RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Ready) - entry.state = RemuxEntryState::Pending; - - // Signal that the insertion point no longer exists. - beginRemoveRows(QModelIndex(), queue.length(), queue.length()); - endRemoveRows(); - - isProcessing = true; - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -void RemuxQueueModel::endProcessing() -{ - for (RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::Ready; - } - } - - // Signal that the insertion point exists again. - isProcessing = false; - if (!autoRemux) { - beginInsertRows(QModelIndex(), queue.length(), queue.length()); - endInsertRows(); - } - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) -{ - bool anyStarted = false; - - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::InProgress; - - inputPath = entry.sourcePath; - outputPath = entry.targetPath; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - anyStarted = true; - break; - } - } - - return anyStarted; -} - -void RemuxQueueModel::finishEntry(bool success) -{ - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::InProgress) { - if (success) - entry.state = RemuxEntryState::Complete; - else - entry.state = RemuxEntryState::Error; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - break; - } - } -} - -/********************************************************** - The actual remux window implementation -**********************************************************/ - -OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) - : QDialog(parent), - queueModel(new RemuxQueueModel), - worker(new RemuxWorker()), - ui(new Ui::OBSRemux), - recPath(path), - autoRemux(autoRemux_) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - - if (autoRemux) { - resize(280, 40); - ui->tableView->hide(); - ui->buttonBox->hide(); - ui->label->hide(); - } - - ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(1000); - ui->progressBar->setValue(0); - - ui->tableView->setModel(queueModel); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, - new RemuxEntryPathItemDelegate(false, recPath)); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, - new RemuxEntryPathItemDelegate(true, recPath)); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, - QHeaderView::ResizeMode::Fixed); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - ui->tableView->setTextElideMode(Qt::ElideMiddle); - ui->tableView->setWordWrap(false); - - installEventFilter(CreateShortcutFilter()); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); - connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); - connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, - &OBSRemux::clearAll); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); - - worker->moveToThread(&remuxer); - remuxer.start(); - - connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); - connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); - connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); - connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); - - connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); - connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); - - QModelIndex index = queueModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -bool OBSRemux::stopRemux() -{ - if (!worker->isWorking) - return true; - - // By locking the worker thread's mutex, we ensure that its - // update poll will be blocked as long as we're in here with - // the popup open. - QMutexLocker lock(&worker->updateMutex); - - bool exit = false; - - if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { - exit = true; - } - - if (exit) { - // Inform the worker it should no longer be - // working. It will interrupt accordingly in - // its next update callback. - worker->isWorking = false; - } - - return exit; -} - -OBSRemux::~OBSRemux() -{ - stopRemux(); - remuxer.quit(); - remuxer.wait(); -} - -void OBSRemux::rowCountChanged(const QModelIndex &, int, int) -{ - // See if there are still any rows ready to remux. Change - // the state of the "go" button accordingly. - // There must be more than one row, since there will always be - // at least one row for the empty insertion point. - if (queueModel->rowCount() > 1) { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - } else { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); - } -} - -void OBSRemux::dropEvent(QDropEvent *ev) -{ - QStringList urlList; - - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - - if (fileInfo.isDir()) { - QStringList directoryFilter; - directoryFilter << "*.flv" - << "*.mp4" - << "*.mov" - << "*.mkv" - << "*.ts" - << "*.m3u8"; - - QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, - QDirIterator::Subdirectories); - - while (dirIter.hasNext()) { - urlList.append(dirIter.next()); - } - } else { - urlList.append(fileInfo.canonicalFilePath()); - } - } - - if (urlList.empty()) { - QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), - QMessageBox::Ok); - } else if (!autoRemux) { - QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); - queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); - } -} - -void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls() && !worker->isWorking) - ev->accept(); -} - -void OBSRemux::beginRemux() -{ - if (worker->isWorking) { - stopRemux(); - return; - } - - bool proceedWithRemux = true; - QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); - - if (!overwriteFiles.empty()) { - QString message = QTStr("Remux.FileExists"); - message += "\n\n"; - - for (QFileInfo fileInfo : overwriteFiles) - message += fileInfo.canonicalFilePath() + "\n"; - - if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) - proceedWithRemux = false; - } - - if (!proceedWithRemux) - return; - - // Set all jobs to "pending" first. - queueModel->beginProcessing(); - - ui->progressBar->setVisible(true); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); - setAcceptDrops(false); - - remuxNextEntry(); -} - -void OBSRemux::AutoRemux(QString inFile, QString outFile) -{ - if (inFile != "" && outFile != "" && autoRemux) { - ui->progressBar->setVisible(true); - emit remux(inFile, outFile); - autoRemuxFile = outFile; - } -} - -void OBSRemux::remuxNextEntry() -{ - worker->lastProgress = 0.f; - - QString inputPath, outputPath; - if (queueModel->beginNextEntry(inputPath, outputPath)) { - emit remux(inputPath, outputPath); - } else { - queueModel->autoRemux = autoRemux; - queueModel->endProcessing(); - - if (!autoRemux) { - OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), - queueModel->checkForErrors() ? QTStr("Remux.FinishedError") - : QTStr("Remux.Finished")); - } - - ui->progressBar->setVisible(autoRemux); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - setAcceptDrops(true); - } -} - -void OBSRemux::closeEvent(QCloseEvent *event) -{ - if (!stopRemux()) - event->ignore(); - else - QDialog::closeEvent(event); -} - -void OBSRemux::reject() -{ - if (!stopRemux()) - return; - - QDialog::reject(); -} - -void OBSRemux::updateProgress(float percent) -{ - ui->progressBar->setValue(percent * 10); -} - -void OBSRemux::remuxFinished(bool success) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - - queueModel->finishEntry(success); - - if (autoRemux && autoRemuxFile != "") { - QTimer::singleShot(3000, this, &OBSRemux::close); - - OBSBasic *main = OBSBasic::Get(); - main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); - } - - remuxNextEntry(); -} - -void OBSRemux::clearFinished() -{ - queueModel->clearFinished(); -} - -void OBSRemux::clearAll() -{ - queueModel->clearAll(); -} - -/********************************************************** - Worker thread - Executes the libobs remux operation as a - background process. -**********************************************************/ - -void RemuxWorker::UpdateProgress(float percent) -{ - if (abs(lastProgress - percent) < 0.1f) - return; - - emit updateProgress(percent); - lastProgress = percent; -} - -void RemuxWorker::remux(const QString &source, const QString &target) -{ - isWorking = true; - - auto callback = [](void *data, float percent) { - RemuxWorker *rw = static_cast(data); - - QMutexLocker lock(&rw->updateMutex); - - rw->UpdateProgress(percent); - - return rw->isWorking; - }; - - bool stopped = false; - bool success = false; - - media_remux_job_t mr_job = nullptr; - if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { - - success = media_remux_job_process(mr_job, callback, this); - - media_remux_job_destroy(mr_job); - - stopped = !isWorking; - } - - isWorking = false; - - emit remuxFinished(!stopped && success); -} diff --git a/frontend/utility/RemuxEntryPathItemDelegate.hpp b/frontend/utility/RemuxEntryPathItemDelegate.hpp index 20a13a780..dcbd7086f 100644 --- a/frontend/utility/RemuxEntryPathItemDelegate.hpp +++ b/frontend/utility/RemuxEntryPathItemDelegate.hpp @@ -17,134 +17,7 @@ #pragma once -#include -#include -#include -#include #include -#include -#include "ui_OBSRemux.h" - -#include -#include - -class RemuxQueueModel; -class RemuxWorker; - -enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; -Q_DECLARE_METATYPE(RemuxEntryState); - -class OBSRemux : public QDialog { - Q_OBJECT - - QPointer queueModel; - QThread remuxer; - QPointer worker; - - std::unique_ptr ui; - - const char *recPath; - - virtual void closeEvent(QCloseEvent *event) override; - virtual void reject() override; - - bool autoRemux; - QString autoRemuxFile; - -public: - explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); - virtual ~OBSRemux() override; - - using job_t = std::shared_ptr; - - void AutoRemux(QString inFile, QString outFile); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - - void remuxNextEntry(); - -private slots: - void rowCountChanged(const QModelIndex &parent, int first, int last); - -public slots: - void updateProgress(float percent); - void remuxFinished(bool success); - void beginRemux(); - bool stopRemux(); - void clearFinished(); - void clearAll(); - -signals: - void remux(const QString &source, const QString &target); -}; - -class RemuxQueueModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSRemux; - -public: - RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - QFileInfoList checkForOverwrites() const; - bool checkForErrors() const; - void beginProcessing(); - void endProcessing(); - bool beginNextEntry(QString &inputPath, QString &outputPath); - void finishEntry(bool success); - bool canClearFinished() const; - void clearFinished(); - void clearAll(); - - bool autoRemux = false; - -private: - struct RemuxQueueEntry { - RemuxEntryState state; - - QString sourcePath; - QString targetPath; - }; - - QList queue; - bool isProcessing; - - static QVariant getIcon(RemuxEntryState state); - - void checkInputPath(int row); -}; - -class RemuxWorker : public QObject { - Q_OBJECT - - QMutex updateMutex; - - bool isWorking; - - float lastProgress; - void UpdateProgress(float percent); - - explicit RemuxWorker() : isWorking(false) {} - virtual ~RemuxWorker(){}; - -private slots: - void remux(const QString &source, const QString &target); - -signals: - void updateProgress(float percent); - void remuxFinished(bool success); - - friend class OBSRemux; -}; class RemuxEntryPathItemDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/utility/RemuxQueueModel.cpp b/frontend/utility/RemuxQueueModel.cpp index d72ebbd9c..cae17b138 100644 --- a/frontend/utility/RemuxQueueModel.cpp +++ b/frontend/utility/RemuxQueueModel.cpp @@ -15,230 +15,14 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-remux.cpp" +#include "RemuxQueueModel.hpp" -#include "obs-app.hpp" +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include -#include "window-basic-main.hpp" - -#include -#include - -using namespace std; - -enum RemuxEntryColumn { - State, - InputPath, - OutputPath, - - Count -}; - -enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { - // Never allow modification of rows that are - // in progress. - return Q_NULLPTR; - } else if (isOutput && state != RemuxEntryState::Ready) { - // Do not allow modification of output rows - // that aren't associated with a valid input. - return Q_NULLPTR; - } else if (!isOutput && state == RemuxEntryState::Complete) { - // Don't allow modification of rows that are - // already complete. - return Q_NULLPTR; - } else { - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!isOutput && state != RemuxEntryState::Empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; - } -} - -void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - if (list.size() > 0) - model->setData(index, list); - } else - model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - if (isOutput) { - if (state != Ready) { - QColor background = - localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); - - localOption.backgroundBrush = QBrush(background); - } - } - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty()) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } else { - QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, - QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - } - - if (isSet) - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/********************************************************** - Model - Manages the queue's data -**********************************************************/ +#include "moc_RemuxQueueModel.cpp" int RemuxQueueModel::rowCount(const QModelIndex &) const { @@ -594,331 +378,3 @@ void RemuxQueueModel::finishEntry(bool success) } } } - -/********************************************************** - The actual remux window implementation -**********************************************************/ - -OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) - : QDialog(parent), - queueModel(new RemuxQueueModel), - worker(new RemuxWorker()), - ui(new Ui::OBSRemux), - recPath(path), - autoRemux(autoRemux_) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - - if (autoRemux) { - resize(280, 40); - ui->tableView->hide(); - ui->buttonBox->hide(); - ui->label->hide(); - } - - ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(1000); - ui->progressBar->setValue(0); - - ui->tableView->setModel(queueModel); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, - new RemuxEntryPathItemDelegate(false, recPath)); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, - new RemuxEntryPathItemDelegate(true, recPath)); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, - QHeaderView::ResizeMode::Fixed); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - ui->tableView->setTextElideMode(Qt::ElideMiddle); - ui->tableView->setWordWrap(false); - - installEventFilter(CreateShortcutFilter()); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); - connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); - connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, - &OBSRemux::clearAll); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); - - worker->moveToThread(&remuxer); - remuxer.start(); - - connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); - connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); - connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); - connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); - - connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); - connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); - - QModelIndex index = queueModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -bool OBSRemux::stopRemux() -{ - if (!worker->isWorking) - return true; - - // By locking the worker thread's mutex, we ensure that its - // update poll will be blocked as long as we're in here with - // the popup open. - QMutexLocker lock(&worker->updateMutex); - - bool exit = false; - - if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { - exit = true; - } - - if (exit) { - // Inform the worker it should no longer be - // working. It will interrupt accordingly in - // its next update callback. - worker->isWorking = false; - } - - return exit; -} - -OBSRemux::~OBSRemux() -{ - stopRemux(); - remuxer.quit(); - remuxer.wait(); -} - -void OBSRemux::rowCountChanged(const QModelIndex &, int, int) -{ - // See if there are still any rows ready to remux. Change - // the state of the "go" button accordingly. - // There must be more than one row, since there will always be - // at least one row for the empty insertion point. - if (queueModel->rowCount() > 1) { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - } else { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); - } -} - -void OBSRemux::dropEvent(QDropEvent *ev) -{ - QStringList urlList; - - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - - if (fileInfo.isDir()) { - QStringList directoryFilter; - directoryFilter << "*.flv" - << "*.mp4" - << "*.mov" - << "*.mkv" - << "*.ts" - << "*.m3u8"; - - QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, - QDirIterator::Subdirectories); - - while (dirIter.hasNext()) { - urlList.append(dirIter.next()); - } - } else { - urlList.append(fileInfo.canonicalFilePath()); - } - } - - if (urlList.empty()) { - QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), - QMessageBox::Ok); - } else if (!autoRemux) { - QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); - queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); - } -} - -void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls() && !worker->isWorking) - ev->accept(); -} - -void OBSRemux::beginRemux() -{ - if (worker->isWorking) { - stopRemux(); - return; - } - - bool proceedWithRemux = true; - QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); - - if (!overwriteFiles.empty()) { - QString message = QTStr("Remux.FileExists"); - message += "\n\n"; - - for (QFileInfo fileInfo : overwriteFiles) - message += fileInfo.canonicalFilePath() + "\n"; - - if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) - proceedWithRemux = false; - } - - if (!proceedWithRemux) - return; - - // Set all jobs to "pending" first. - queueModel->beginProcessing(); - - ui->progressBar->setVisible(true); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); - setAcceptDrops(false); - - remuxNextEntry(); -} - -void OBSRemux::AutoRemux(QString inFile, QString outFile) -{ - if (inFile != "" && outFile != "" && autoRemux) { - ui->progressBar->setVisible(true); - emit remux(inFile, outFile); - autoRemuxFile = outFile; - } -} - -void OBSRemux::remuxNextEntry() -{ - worker->lastProgress = 0.f; - - QString inputPath, outputPath; - if (queueModel->beginNextEntry(inputPath, outputPath)) { - emit remux(inputPath, outputPath); - } else { - queueModel->autoRemux = autoRemux; - queueModel->endProcessing(); - - if (!autoRemux) { - OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), - queueModel->checkForErrors() ? QTStr("Remux.FinishedError") - : QTStr("Remux.Finished")); - } - - ui->progressBar->setVisible(autoRemux); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - setAcceptDrops(true); - } -} - -void OBSRemux::closeEvent(QCloseEvent *event) -{ - if (!stopRemux()) - event->ignore(); - else - QDialog::closeEvent(event); -} - -void OBSRemux::reject() -{ - if (!stopRemux()) - return; - - QDialog::reject(); -} - -void OBSRemux::updateProgress(float percent) -{ - ui->progressBar->setValue(percent * 10); -} - -void OBSRemux::remuxFinished(bool success) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - - queueModel->finishEntry(success); - - if (autoRemux && autoRemuxFile != "") { - QTimer::singleShot(3000, this, &OBSRemux::close); - - OBSBasic *main = OBSBasic::Get(); - main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); - } - - remuxNextEntry(); -} - -void OBSRemux::clearFinished() -{ - queueModel->clearFinished(); -} - -void OBSRemux::clearAll() -{ - queueModel->clearAll(); -} - -/********************************************************** - Worker thread - Executes the libobs remux operation as a - background process. -**********************************************************/ - -void RemuxWorker::UpdateProgress(float percent) -{ - if (abs(lastProgress - percent) < 0.1f) - return; - - emit updateProgress(percent); - lastProgress = percent; -} - -void RemuxWorker::remux(const QString &source, const QString &target) -{ - isWorking = true; - - auto callback = [](void *data, float percent) { - RemuxWorker *rw = static_cast(data); - - QMutexLocker lock(&rw->updateMutex); - - rw->UpdateProgress(percent); - - return rw->isWorking; - }; - - bool stopped = false; - bool success = false; - - media_remux_job_t mr_job = nullptr; - if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { - - success = media_remux_job_process(mr_job, callback, this); - - media_remux_job_destroy(mr_job); - - stopped = !isWorking; - } - - isWorking = false; - - emit remuxFinished(!stopped && success); -} diff --git a/frontend/utility/RemuxQueueModel.hpp b/frontend/utility/RemuxQueueModel.hpp index 20a13a780..954b5fd5e 100644 --- a/frontend/utility/RemuxQueueModel.hpp +++ b/frontend/utility/RemuxQueueModel.hpp @@ -17,69 +17,23 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include "ui_OBSRemux.h" - -#include -#include - -class RemuxQueueModel; -class RemuxWorker; +#include +#include enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; + Q_DECLARE_METATYPE(RemuxEntryState); -class OBSRemux : public QDialog { - Q_OBJECT +enum RemuxEntryColumn { + State, + InputPath, + OutputPath, - QPointer queueModel; - QThread remuxer; - QPointer worker; - - std::unique_ptr ui; - - const char *recPath; - - virtual void closeEvent(QCloseEvent *event) override; - virtual void reject() override; - - bool autoRemux; - QString autoRemuxFile; - -public: - explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); - virtual ~OBSRemux() override; - - using job_t = std::shared_ptr; - - void AutoRemux(QString inFile, QString outFile); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - - void remuxNextEntry(); - -private slots: - void rowCountChanged(const QModelIndex &parent, int first, int last); - -public slots: - void updateProgress(float percent); - void remuxFinished(bool success); - void beginRemux(); - bool stopRemux(); - void clearFinished(); - void clearAll(); - -signals: - void remux(const QString &source, const QString &target); + Count }; +enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + class RemuxQueueModel : public QAbstractTableModel { Q_OBJECT @@ -122,52 +76,3 @@ private: void checkInputPath(int row); }; - -class RemuxWorker : public QObject { - Q_OBJECT - - QMutex updateMutex; - - bool isWorking; - - float lastProgress; - void UpdateProgress(float percent); - - explicit RemuxWorker() : isWorking(false) {} - virtual ~RemuxWorker(){}; - -private slots: - void remux(const QString &source, const QString &target); - -signals: - void updateProgress(float percent); - void remuxFinished(bool success); - - friend class OBSRemux; -}; - -class RemuxEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; diff --git a/frontend/utility/RemuxWorker.cpp b/frontend/utility/RemuxWorker.cpp index d72ebbd9c..e9d54093b 100644 --- a/frontend/utility/RemuxWorker.cpp +++ b/frontend/utility/RemuxWorker.cpp @@ -15,873 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-remux.cpp" +#include "RemuxWorker.hpp" -#include "obs-app.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include "window-basic-main.hpp" - -#include -#include - -using namespace std; - -enum RemuxEntryColumn { - State, - InputPath, - OutputPath, - - Count -}; - -enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { - // Never allow modification of rows that are - // in progress. - return Q_NULLPTR; - } else if (isOutput && state != RemuxEntryState::Ready) { - // Do not allow modification of output rows - // that aren't associated with a valid input. - return Q_NULLPTR; - } else if (!isOutput && state == RemuxEntryState::Complete) { - // Don't allow modification of rows that are - // already complete. - return Q_NULLPTR; - } else { - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!isOutput && state != RemuxEntryState::Empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; - } -} - -void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - if (list.size() > 0) - model->setData(index, list); - } else - model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - if (isOutput) { - if (state != Ready) { - QColor background = - localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); - - localOption.backgroundBrush = QBrush(background); - } - } - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty()) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } else { - QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, - QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - } - - if (isSet) - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/********************************************************** - Model - Manages the queue's data -**********************************************************/ - -int RemuxQueueModel::rowCount(const QModelIndex &) const -{ - return queue.length() + (isProcessing ? 0 : 1); -} - -int RemuxQueueModel::columnCount(const QModelIndex &) const -{ - return RemuxEntryColumn::Count; -} - -QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= queue.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - result = queue[index.row()].sourcePath; - break; - case RemuxEntryColumn::OutputPath: - result = queue[index.row()].targetPath; - break; - } - } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { - result = getIcon(queue[index.row()].state); - } else if (role == RemuxEntryRole::EntryStateRole) { - result = queue[index.row()].state; - } - - return result; -} - -QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case RemuxEntryColumn::State: - result = QString(); - break; - case RemuxEntryColumn::InputPath: - result = QTStr("Remux.SourceFile"); - break; - case RemuxEntryColumn::OutputPath: - result = QTStr("Remux.TargetFile"); - break; - } - } - - return result; -} - -Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == RemuxEntryColumn::InputPath) { - flags |= Qt::ItemIsEditable; - } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == RemuxEntryRole::NewPathsToProcessRole) { - QStringList pathList = value.toStringList(); - - if (pathList.size() == 0) { - if (index.row() < queue.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (pathList.size() >= 1 && index.row() < queue.length()) { - queue[index.row()].sourcePath = pathList[0]; - checkInputPath(index.row()); - - pathList.removeAt(0); - - success = true; - } - - if (pathList.size() > 0) { - int row = index.row(); - int lastRow = row + pathList.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : pathList) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - queue.insert(row, entry); - row++; - } - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - - success = true; - } - } - } else if (index.row() == queue.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); - queue.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - success = true; - } - } else { - QString path = value.toString(); - - if (path.isEmpty()) { - if (index.column() == RemuxEntryColumn::InputPath) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - queue[index.row()].sourcePath = value.toString(); - checkInputPath(index.row()); - success = true; - break; - case RemuxEntryColumn::OutputPath: - queue[index.row()].targetPath = value.toString(); - emit dataChanged(index, index); - success = true; - break; - } - } - } - - return success; -} - -QVariant RemuxQueueModel::getIcon(RemuxEntryState state) -{ - QVariant icon; - QStyle *style = QApplication::style(); - - switch (state) { - case RemuxEntryState::Complete: - icon = style->standardIcon(QStyle::SP_DialogApplyButton); - break; - - case RemuxEntryState::InProgress: - icon = style->standardIcon(QStyle::SP_ArrowRight); - break; - - case RemuxEntryState::Error: - icon = style->standardIcon(QStyle::SP_DialogCancelButton); - break; - - case RemuxEntryState::InvalidPath: - icon = style->standardIcon(QStyle::SP_MessageBoxWarning); - break; - - default: - break; - } - - return icon; -} - -void RemuxQueueModel::checkInputPath(int row) -{ - RemuxQueueEntry &entry = queue[row]; - - if (entry.sourcePath.isEmpty()) { - entry.state = RemuxEntryState::Empty; - } else { - entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); - QFileInfo fileInfo(entry.sourcePath); - if (fileInfo.exists()) - entry.state = RemuxEntryState::Ready; - else - entry.state = RemuxEntryState::InvalidPath; - - QString newExt = ".mp4"; - QString suffix = fileInfo.suffix(); - - if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { - newExt = ".remuxed." + suffix; - } - - if (entry.state == RemuxEntryState::Ready) - entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + - fileInfo.completeBaseName() + newExt); - } - - if (entry.state == RemuxEntryState::Ready && isProcessing) - entry.state = RemuxEntryState::Pending; - - emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); -} - -QFileInfoList RemuxQueueModel::checkForOverwrites() const -{ - QFileInfoList list; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Ready) { - QFileInfo fileInfo(entry.targetPath); - if (fileInfo.exists()) { - list.append(fileInfo); - } - } - } - - return list; -} - -bool RemuxQueueModel::checkForErrors() const -{ - bool hasErrors = false; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Error) { - hasErrors = true; - break; - } - } - - return hasErrors; -} - -void RemuxQueueModel::clearAll() -{ - beginRemoveRows(QModelIndex(), 0, queue.size() - 1); - queue.clear(); - endRemoveRows(); -} - -void RemuxQueueModel::clearFinished() -{ - int index = 0; - - for (index = 0; index < queue.size(); index++) { - const RemuxQueueEntry &entry = queue[index]; - if (entry.state == RemuxEntryState::Complete) { - beginRemoveRows(QModelIndex(), index, index); - queue.removeAt(index); - endRemoveRows(); - index--; - } - } -} - -bool RemuxQueueModel::canClearFinished() const -{ - bool canClearFinished = false; - for (const RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Complete) { - canClearFinished = true; - break; - } - - return canClearFinished; -} - -void RemuxQueueModel::beginProcessing() -{ - for (RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Ready) - entry.state = RemuxEntryState::Pending; - - // Signal that the insertion point no longer exists. - beginRemoveRows(QModelIndex(), queue.length(), queue.length()); - endRemoveRows(); - - isProcessing = true; - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -void RemuxQueueModel::endProcessing() -{ - for (RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::Ready; - } - } - - // Signal that the insertion point exists again. - isProcessing = false; - if (!autoRemux) { - beginInsertRows(QModelIndex(), queue.length(), queue.length()); - endInsertRows(); - } - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) -{ - bool anyStarted = false; - - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::InProgress; - - inputPath = entry.sourcePath; - outputPath = entry.targetPath; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - anyStarted = true; - break; - } - } - - return anyStarted; -} - -void RemuxQueueModel::finishEntry(bool success) -{ - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::InProgress) { - if (success) - entry.state = RemuxEntryState::Complete; - else - entry.state = RemuxEntryState::Error; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - break; - } - } -} - -/********************************************************** - The actual remux window implementation -**********************************************************/ - -OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) - : QDialog(parent), - queueModel(new RemuxQueueModel), - worker(new RemuxWorker()), - ui(new Ui::OBSRemux), - recPath(path), - autoRemux(autoRemux_) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - - if (autoRemux) { - resize(280, 40); - ui->tableView->hide(); - ui->buttonBox->hide(); - ui->label->hide(); - } - - ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(1000); - ui->progressBar->setValue(0); - - ui->tableView->setModel(queueModel); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, - new RemuxEntryPathItemDelegate(false, recPath)); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, - new RemuxEntryPathItemDelegate(true, recPath)); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, - QHeaderView::ResizeMode::Fixed); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - ui->tableView->setTextElideMode(Qt::ElideMiddle); - ui->tableView->setWordWrap(false); - - installEventFilter(CreateShortcutFilter()); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); - connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); - connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, - &OBSRemux::clearAll); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); - - worker->moveToThread(&remuxer); - remuxer.start(); - - connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); - connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); - connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); - connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); - - connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); - connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); - - QModelIndex index = queueModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -bool OBSRemux::stopRemux() -{ - if (!worker->isWorking) - return true; - - // By locking the worker thread's mutex, we ensure that its - // update poll will be blocked as long as we're in here with - // the popup open. - QMutexLocker lock(&worker->updateMutex); - - bool exit = false; - - if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { - exit = true; - } - - if (exit) { - // Inform the worker it should no longer be - // working. It will interrupt accordingly in - // its next update callback. - worker->isWorking = false; - } - - return exit; -} - -OBSRemux::~OBSRemux() -{ - stopRemux(); - remuxer.quit(); - remuxer.wait(); -} - -void OBSRemux::rowCountChanged(const QModelIndex &, int, int) -{ - // See if there are still any rows ready to remux. Change - // the state of the "go" button accordingly. - // There must be more than one row, since there will always be - // at least one row for the empty insertion point. - if (queueModel->rowCount() > 1) { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - } else { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); - } -} - -void OBSRemux::dropEvent(QDropEvent *ev) -{ - QStringList urlList; - - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - - if (fileInfo.isDir()) { - QStringList directoryFilter; - directoryFilter << "*.flv" - << "*.mp4" - << "*.mov" - << "*.mkv" - << "*.ts" - << "*.m3u8"; - - QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, - QDirIterator::Subdirectories); - - while (dirIter.hasNext()) { - urlList.append(dirIter.next()); - } - } else { - urlList.append(fileInfo.canonicalFilePath()); - } - } - - if (urlList.empty()) { - QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), - QMessageBox::Ok); - } else if (!autoRemux) { - QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); - queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); - } -} - -void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls() && !worker->isWorking) - ev->accept(); -} - -void OBSRemux::beginRemux() -{ - if (worker->isWorking) { - stopRemux(); - return; - } - - bool proceedWithRemux = true; - QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); - - if (!overwriteFiles.empty()) { - QString message = QTStr("Remux.FileExists"); - message += "\n\n"; - - for (QFileInfo fileInfo : overwriteFiles) - message += fileInfo.canonicalFilePath() + "\n"; - - if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) - proceedWithRemux = false; - } - - if (!proceedWithRemux) - return; - - // Set all jobs to "pending" first. - queueModel->beginProcessing(); - - ui->progressBar->setVisible(true); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); - setAcceptDrops(false); - - remuxNextEntry(); -} - -void OBSRemux::AutoRemux(QString inFile, QString outFile) -{ - if (inFile != "" && outFile != "" && autoRemux) { - ui->progressBar->setVisible(true); - emit remux(inFile, outFile); - autoRemuxFile = outFile; - } -} - -void OBSRemux::remuxNextEntry() -{ - worker->lastProgress = 0.f; - - QString inputPath, outputPath; - if (queueModel->beginNextEntry(inputPath, outputPath)) { - emit remux(inputPath, outputPath); - } else { - queueModel->autoRemux = autoRemux; - queueModel->endProcessing(); - - if (!autoRemux) { - OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), - queueModel->checkForErrors() ? QTStr("Remux.FinishedError") - : QTStr("Remux.Finished")); - } - - ui->progressBar->setVisible(autoRemux); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - setAcceptDrops(true); - } -} - -void OBSRemux::closeEvent(QCloseEvent *event) -{ - if (!stopRemux()) - event->ignore(); - else - QDialog::closeEvent(event); -} - -void OBSRemux::reject() -{ - if (!stopRemux()) - return; - - QDialog::reject(); -} - -void OBSRemux::updateProgress(float percent) -{ - ui->progressBar->setValue(percent * 10); -} - -void OBSRemux::remuxFinished(bool success) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - - queueModel->finishEntry(success); - - if (autoRemux && autoRemuxFile != "") { - QTimer::singleShot(3000, this, &OBSRemux::close); - - OBSBasic *main = OBSBasic::Get(); - main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); - } - - remuxNextEntry(); -} - -void OBSRemux::clearFinished() -{ - queueModel->clearFinished(); -} - -void OBSRemux::clearAll() -{ - queueModel->clearAll(); -} - -/********************************************************** - Worker thread - Executes the libobs remux operation as a - background process. -**********************************************************/ - void RemuxWorker::UpdateProgress(float percent) { if (abs(lastProgress - percent) < 0.1f) diff --git a/frontend/utility/RemuxWorker.hpp b/frontend/utility/RemuxWorker.hpp index 20a13a780..9816e3501 100644 --- a/frontend/utility/RemuxWorker.hpp +++ b/frontend/utility/RemuxWorker.hpp @@ -17,111 +17,8 @@ #pragma once -#include #include -#include -#include -#include -#include -#include "ui_OBSRemux.h" - -#include -#include - -class RemuxQueueModel; -class RemuxWorker; - -enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; -Q_DECLARE_METATYPE(RemuxEntryState); - -class OBSRemux : public QDialog { - Q_OBJECT - - QPointer queueModel; - QThread remuxer; - QPointer worker; - - std::unique_ptr ui; - - const char *recPath; - - virtual void closeEvent(QCloseEvent *event) override; - virtual void reject() override; - - bool autoRemux; - QString autoRemuxFile; - -public: - explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); - virtual ~OBSRemux() override; - - using job_t = std::shared_ptr; - - void AutoRemux(QString inFile, QString outFile); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - - void remuxNextEntry(); - -private slots: - void rowCountChanged(const QModelIndex &parent, int first, int last); - -public slots: - void updateProgress(float percent); - void remuxFinished(bool success); - void beginRemux(); - bool stopRemux(); - void clearFinished(); - void clearAll(); - -signals: - void remux(const QString &source, const QString &target); -}; - -class RemuxQueueModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSRemux; - -public: - RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - QFileInfoList checkForOverwrites() const; - bool checkForErrors() const; - void beginProcessing(); - void endProcessing(); - bool beginNextEntry(QString &inputPath, QString &outputPath); - void finishEntry(bool success); - bool canClearFinished() const; - void clearFinished(); - void clearAll(); - - bool autoRemux = false; - -private: - struct RemuxQueueEntry { - RemuxEntryState state; - - QString sourcePath; - QString targetPath; - }; - - QList queue; - bool isProcessing; - - static QVariant getIcon(RemuxEntryState state); - - void checkInputPath(int row); -}; +#include class RemuxWorker : public QObject { Q_OBJECT @@ -145,29 +42,3 @@ signals: friend class OBSRemux; }; - -class RemuxEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; From a959ecadaf5764a6bf224e511737f0695c1900ba Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 18:39:31 +0100 Subject: [PATCH 07/37] frontend: Add renamed Qt UI docks --- .../docks/BrowserDock.cpp | 3 ++- .../docks/BrowserDock.hpp | 4 ++-- .../docks/OBSDock.cpp | 10 ++++++---- .../docks/OBSDock.hpp | 4 ++++ .../docks/YouTubeAppDock.cpp | 20 +++++++++++-------- .../docks/YouTubeAppDock.hpp | 6 ++++-- 6 files changed, 30 insertions(+), 17 deletions(-) rename UI/window-dock-browser.cpp => frontend/docks/BrowserDock.cpp (92%) rename UI/window-dock-browser.hpp => frontend/docks/BrowserDock.hpp (92%) rename UI/window-dock.cpp => frontend/docks/OBSDock.cpp (92%) rename UI/window-dock.hpp => frontend/docks/OBSDock.hpp (86%) rename UI/window-dock-youtube-app.cpp => frontend/docks/YouTubeAppDock.cpp (98%) rename UI/window-dock-youtube-app.hpp => frontend/docks/YouTubeAppDock.hpp (94%) diff --git a/UI/window-dock-browser.cpp b/frontend/docks/BrowserDock.cpp similarity index 92% rename from UI/window-dock-browser.cpp rename to frontend/docks/BrowserDock.cpp index 30e1de1bc..622d94bd6 100644 --- a/UI/window-dock-browser.cpp +++ b/frontend/docks/BrowserDock.cpp @@ -1,4 +1,5 @@ -#include "window-dock-browser.hpp" +#include "BrowserDock.hpp" + #include void BrowserDock::closeEvent(QCloseEvent *event) diff --git a/UI/window-dock-browser.hpp b/frontend/docks/BrowserDock.hpp similarity index 92% rename from UI/window-dock-browser.hpp rename to frontend/docks/BrowserDock.hpp index 750ed42bd..a62f9c26a 100644 --- a/UI/window-dock-browser.hpp +++ b/frontend/docks/BrowserDock.hpp @@ -1,9 +1,9 @@ #pragma once -#include "window-dock.hpp" -#include +#include "OBSDock.hpp" #include + extern QCef *cef; extern QCefCookieManager *panel_cookies; diff --git a/UI/window-dock.cpp b/frontend/docks/OBSDock.cpp similarity index 92% rename from UI/window-dock.cpp rename to frontend/docks/OBSDock.cpp index 8755dd097..c0271b61f 100644 --- a/UI/window-dock.cpp +++ b/frontend/docks/OBSDock.cpp @@ -1,9 +1,11 @@ -#include "moc_window-dock.cpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" +#include "OBSDock.hpp" + +#include -#include #include +#include + +#include "moc_OBSDock.cpp" void OBSDock::closeEvent(QCloseEvent *event) { diff --git a/UI/window-dock.hpp b/frontend/docks/OBSDock.hpp similarity index 86% rename from UI/window-dock.hpp rename to frontend/docks/OBSDock.hpp index 6d843a419..5eddc3d4a 100644 --- a/UI/window-dock.hpp +++ b/frontend/docks/OBSDock.hpp @@ -2,6 +2,10 @@ #include +class QCloseEvent; +class QShowEvent; +class QString; + class OBSDock : public QDockWidget { Q_OBJECT diff --git a/UI/window-dock-youtube-app.cpp b/frontend/docks/YouTubeAppDock.cpp similarity index 98% rename from UI/window-dock-youtube-app.cpp rename to frontend/docks/YouTubeAppDock.cpp index 5a2ec34de..a5b283484 100644 --- a/UI/window-dock-youtube-app.cpp +++ b/frontend/docks/YouTubeAppDock.cpp @@ -1,15 +1,19 @@ +#include "YouTubeAppDock.hpp" + +#include +#include + +#include + #include - -#include "window-basic-main.hpp" -#include "youtube-api-wrappers.hpp" -#include "moc_window-dock-youtube-app.cpp" - -#include "ui-config.h" -#include "qt-wrappers.hpp" - #include + +#include "moc_YouTubeAppDock.cpp" + using json = nlohmann::json; +extern bool cef_js_avail; + #ifdef YOUTUBE_WEBAPP_PLACEHOLDER static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL = YOUTUBE_WEBAPP_PLACEHOLDER; #else diff --git a/UI/window-dock-youtube-app.hpp b/frontend/docks/YouTubeAppDock.hpp similarity index 94% rename from UI/window-dock-youtube-app.hpp rename to frontend/docks/YouTubeAppDock.hpp index ff6074de7..2e7b79ce4 100644 --- a/UI/window-dock-youtube-app.hpp +++ b/frontend/docks/YouTubeAppDock.hpp @@ -1,10 +1,12 @@ #pragma once -#include "window-dock-browser.hpp" -#include "youtube-api-wrappers.hpp" +#include "BrowserDock.hpp" + +#include class QAction; class QCefWidget; +class YoutubeApiWrappers; class YouTubeAppDock : public BrowserDock { Q_OBJECT From e0469b00d65bf631ea74fc51257138958cb8f2cc Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Thu, 21 Nov 2024 20:36:12 +0100 Subject: [PATCH 08/37] frontend: Add OBSImporter and importer implementations --- .../ImporterEntryPathItemDelegate.cpp | 0 .../ImporterEntryPathItemDelegate.hpp | 0 frontend/importer/ImporterModel.cpp | 570 ++++++++++++++++++ frontend/importer/ImporterModel.hpp | 102 ++++ frontend/importer/OBSImporter.cpp | 570 ++++++++++++++++++ frontend/importer/OBSImporter.hpp | 102 ++++ {UI => frontend}/importers/classic.cpp | 0 {UI => frontend}/importers/importers.cpp | 0 {UI => frontend}/importers/importers.hpp | 0 {UI => frontend}/importers/sl.cpp | 0 {UI => frontend}/importers/studio.cpp | 0 {UI => frontend}/importers/xsplit.cpp | 0 12 files changed, 1344 insertions(+) rename UI/window-importer.cpp => frontend/importer/ImporterEntryPathItemDelegate.cpp (100%) rename UI/window-importer.hpp => frontend/importer/ImporterEntryPathItemDelegate.hpp (100%) create mode 100644 frontend/importer/ImporterModel.cpp create mode 100644 frontend/importer/ImporterModel.hpp create mode 100644 frontend/importer/OBSImporter.cpp create mode 100644 frontend/importer/OBSImporter.hpp rename {UI => frontend}/importers/classic.cpp (100%) rename {UI => frontend}/importers/importers.cpp (100%) rename {UI => frontend}/importers/importers.hpp (100%) rename {UI => frontend}/importers/sl.cpp (100%) rename {UI => frontend}/importers/studio.cpp (100%) rename {UI => frontend}/importers/xsplit.cpp (100%) diff --git a/UI/window-importer.cpp b/frontend/importer/ImporterEntryPathItemDelegate.cpp similarity index 100% rename from UI/window-importer.cpp rename to frontend/importer/ImporterEntryPathItemDelegate.cpp diff --git a/UI/window-importer.hpp b/frontend/importer/ImporterEntryPathItemDelegate.hpp similarity index 100% rename from UI/window-importer.hpp rename to frontend/importer/ImporterEntryPathItemDelegate.hpp diff --git a/frontend/importer/ImporterModel.cpp b/frontend/importer/ImporterModel.cpp new file mode 100644 index 000000000..88d4ad2d9 --- /dev/null +++ b/frontend/importer/ImporterModel.cpp @@ -0,0 +1,570 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + 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 "moc_window-importer.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importers/importers.hpp" + +extern bool SceneCollectionExists(const char *findName); + +enum ImporterColumn { + Selected, + Name, + Path, + Program, + + Count +}; + +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} + +QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + bool empty = index.model() + ->index(index.row(), ImporterColumn::Path) + .data(ImporterEntryRole::CheckEmpty) + .value(); + + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; +} + +void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + model->setData(index, list, ImporterEntryRole::NewPath); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + + bool isSet = false; + QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } + + if (isSet) + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/** + Model +**/ + +int ImporterModel::rowCount(const QModelIndex &) const +{ + return options.length() + 1; +} + +int ImporterModel::columnCount(const QModelIndex &) const +{ + return ImporterColumn::Count; +} + +QVariant ImporterModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= options.length()) { + if (role == ImporterEntryRole::CheckEmpty) + result = true; + else + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case ImporterColumn::Path: + result = options[index.row()].path; + break; + case ImporterColumn::Program: + result = options[index.row()].program; + break; + case ImporterColumn::Name: + result = options[index.row()].name; + } + } else if (role == Qt::EditRole) { + if (index.column() == ImporterColumn::Name) { + result = options[index.row()].name; + } + } else if (role == Qt::CheckStateRole) { + switch (index.column()) { + case ImporterColumn::Selected: + if (options[index.row()].program != "") + result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; + else + result = Qt::Unchecked; + } + } else if (role == ImporterEntryRole::CheckEmpty) { + result = options[index.row()].empty; + } + + return result; +} + +Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { + flags |= Qt::ItemIsUserCheckable; + } else if (index.column() == ImporterColumn::Path || + (index.column() == ImporterColumn::Name && index.row() != options.length())) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void ImporterModel::checkInputPath(int row) +{ + ImporterEntry &entry = options[row]; + + if (entry.path.isEmpty()) { + entry.program = ""; + entry.empty = true; + entry.selected = false; + entry.name = ""; + } else { + entry.empty = false; + + std::string program = DetectProgram(entry.path.toStdString()); + entry.program = QTStr(program.c_str()); + + if (program.empty()) { + entry.selected = false; + } else { + std::string name = GetSCName(entry.path.toStdString(), program); + entry.name = name.c_str(); + } + } + + emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); +} + +bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == ImporterEntryRole::NewPath) { + QStringList list = value.toStringList(); + + if (list.size() == 0) { + if (index.row() < options.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + options.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (list.size() > 0 && index.row() < options.length()) { + options[index.row()].path = list[0]; + checkInputPath(index.row()); + + list.removeAt(0); + } + + if (list.size() > 0) { + int row = index.row(); + int lastRow = row + list.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : list) { + ImporterEntry entry; + entry.path = path; + + options.insert(row, entry); + + row++; + } + + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + } + } + } else if (index.row() == options.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + ImporterEntry entry; + entry.path = path; + entry.selected = role != ImporterEntryRole::AutoPath; + entry.empty = false; + + beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); + options.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + } + } else if (index.column() == ImporterColumn::Selected) { + bool select = value.toBool(); + + options[index.row()].selected = select; + } else if (index.column() == ImporterColumn::Path) { + QString path = value.toString(); + options[index.row()].path = path; + + checkInputPath(index.row()); + } else if (index.column() == ImporterColumn::Name) { + QString name = value.toString(); + options[index.row()].name = name; + } + + emit dataChanged(index, index); + + return true; +} + +QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case ImporterColumn::Path: + result = QTStr("Importer.Path"); + break; + case ImporterColumn::Program: + result = QTStr("Importer.Program"); + break; + case ImporterColumn::Name: + result = QTStr("Name"); + } + } + + return result; +} + +/** + Window +**/ + +OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(optionsModel); + ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); + + connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); + + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); + ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, + &OBSImporter::importCollections); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); + + ImportersInit(); + + bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); + + if (!autoSearchPrompt) { + QMessageBox::StandardButton button = OBSMessageBox::question( + parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); + + if (button == QMessageBox::Yes) { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); + } else { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); + } + + config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); + } + + bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + + OBSImporterFiles f; + if (autoSearch) + f = ImportersFindFiles(); + + for (size_t i = 0; i < f.size(); i++) { + QString path = f[i].c_str(); + path.replace("\\", "/"); + addImportOption(path, true); + } + + f.clear(); + + ui->tableView->resizeColumnsToContents(); + + QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +void OBSImporter::addImportOption(QString path, bool automatic) +{ + QStringList list; + + list.append(path); + + QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); + + optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); +} + +void OBSImporter::dropEvent(QDropEvent *ev) +{ + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + if (fileInfo.isDir()) { + + QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); + + while (dirIter.hasNext()) { + addImportOption(dirIter.next(), false); + } + } else { + addImportOption(fileInfo.canonicalFilePath(), false); + } + } +} + +void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls()) + ev->accept(); +} + +void OBSImporter::browseImport() +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + for (int i = 0; i < paths.count(); i++) { + addImportOption(paths[i], false); + } + } +} + +bool GetUnusedName(std::string &name) +{ + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { + return false; + } + + std::string newName; + int inc = 2; + do { + newName = name; + newName += " "; + newName += std::to_string(inc++); + } while (basic->GetSceneCollectionByName(newName)); + + name = newName; + return true; +} + +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + +void OBSImporter::importCollections() +{ + setEnabled(false); + + const std::filesystem::path sceneCollectionLocation = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); + + for (int i = 0; i < optionsModel->rowCount() - 1; i++) { + int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); + + if (selected == Qt::Unchecked) + continue; + + std::string pathStr = optionsModel->index(i, ImporterColumn::Path) + .data(Qt::DisplayRole) + .value() + .toStdString(); + std::string nameStr = optionsModel->index(i, ImporterColumn::Name) + .data(Qt::DisplayRole) + .value() + .toStdString(); + + json11::Json res; + ImportSC(pathStr, nameStr, res); + + if (res != json11::Json()) { + json11::Json::object out = res.object_items(); + std::string name = res["name"].string_value(); + std::string file; + + if (GetUnusedName(name)) { + json11::Json::object newOut = out; + newOut["name"] = name; + out = newOut; + } + + std::string fileName; + if (!GetFileSafeName(name.c_str(), fileName)) { + blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); + } + + std::string collectionFile; + collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); + collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); + } + + std::string out_str = json11::Json(out).dump(); + + bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), + false); + + blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), + success ? "SUCCESS" : "FAILURE"); + } + } + + close(); +} + +void OBSImporter::dataChanged() +{ + ui->tableView->resizeColumnToContents(ImporterColumn::Name); +} diff --git a/frontend/importer/ImporterModel.hpp b/frontend/importer/ImporterModel.hpp new file mode 100644 index 000000000..8e77db645 --- /dev/null +++ b/frontend/importer/ImporterModel.hpp @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + 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 "obs-app.hpp" +#include "window-basic-main.hpp" +#include +#include +#include +#include "ui_OBSImporter.h" + +class ImporterModel; + +class OBSImporter : public QDialog { + Q_OBJECT + + QPointer optionsModel; + std::unique_ptr ui; + +public: + explicit OBSImporter(QWidget *parent = nullptr); + + void addImportOption(QString path, bool automatic); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + +public slots: + void browseImport(); + void importCollections(); + void dataChanged(); +}; + +class ImporterModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSImporter; + +public: + ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + +private: + struct ImporterEntry { + QString path; + QString program; + QString name; + + bool selected; + bool empty; + }; + + QList options; + + void checkInputPath(int row); +}; + +class ImporterEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + ImporterEntryPathItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/frontend/importer/OBSImporter.cpp b/frontend/importer/OBSImporter.cpp new file mode 100644 index 000000000..88d4ad2d9 --- /dev/null +++ b/frontend/importer/OBSImporter.cpp @@ -0,0 +1,570 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + 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 "moc_window-importer.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importers/importers.hpp" + +extern bool SceneCollectionExists(const char *findName); + +enum ImporterColumn { + Selected, + Name, + Path, + Program, + + Count +}; + +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} + +QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + bool empty = index.model() + ->index(index.row(), ImporterColumn::Path) + .data(ImporterEntryRole::CheckEmpty) + .value(); + + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; +} + +void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + model->setData(index, list, ImporterEntryRole::NewPath); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + + bool isSet = false; + QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } + + if (isSet) + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/** + Model +**/ + +int ImporterModel::rowCount(const QModelIndex &) const +{ + return options.length() + 1; +} + +int ImporterModel::columnCount(const QModelIndex &) const +{ + return ImporterColumn::Count; +} + +QVariant ImporterModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= options.length()) { + if (role == ImporterEntryRole::CheckEmpty) + result = true; + else + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case ImporterColumn::Path: + result = options[index.row()].path; + break; + case ImporterColumn::Program: + result = options[index.row()].program; + break; + case ImporterColumn::Name: + result = options[index.row()].name; + } + } else if (role == Qt::EditRole) { + if (index.column() == ImporterColumn::Name) { + result = options[index.row()].name; + } + } else if (role == Qt::CheckStateRole) { + switch (index.column()) { + case ImporterColumn::Selected: + if (options[index.row()].program != "") + result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; + else + result = Qt::Unchecked; + } + } else if (role == ImporterEntryRole::CheckEmpty) { + result = options[index.row()].empty; + } + + return result; +} + +Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { + flags |= Qt::ItemIsUserCheckable; + } else if (index.column() == ImporterColumn::Path || + (index.column() == ImporterColumn::Name && index.row() != options.length())) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void ImporterModel::checkInputPath(int row) +{ + ImporterEntry &entry = options[row]; + + if (entry.path.isEmpty()) { + entry.program = ""; + entry.empty = true; + entry.selected = false; + entry.name = ""; + } else { + entry.empty = false; + + std::string program = DetectProgram(entry.path.toStdString()); + entry.program = QTStr(program.c_str()); + + if (program.empty()) { + entry.selected = false; + } else { + std::string name = GetSCName(entry.path.toStdString(), program); + entry.name = name.c_str(); + } + } + + emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); +} + +bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == ImporterEntryRole::NewPath) { + QStringList list = value.toStringList(); + + if (list.size() == 0) { + if (index.row() < options.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + options.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (list.size() > 0 && index.row() < options.length()) { + options[index.row()].path = list[0]; + checkInputPath(index.row()); + + list.removeAt(0); + } + + if (list.size() > 0) { + int row = index.row(); + int lastRow = row + list.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : list) { + ImporterEntry entry; + entry.path = path; + + options.insert(row, entry); + + row++; + } + + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + } + } + } else if (index.row() == options.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + ImporterEntry entry; + entry.path = path; + entry.selected = role != ImporterEntryRole::AutoPath; + entry.empty = false; + + beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); + options.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + } + } else if (index.column() == ImporterColumn::Selected) { + bool select = value.toBool(); + + options[index.row()].selected = select; + } else if (index.column() == ImporterColumn::Path) { + QString path = value.toString(); + options[index.row()].path = path; + + checkInputPath(index.row()); + } else if (index.column() == ImporterColumn::Name) { + QString name = value.toString(); + options[index.row()].name = name; + } + + emit dataChanged(index, index); + + return true; +} + +QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case ImporterColumn::Path: + result = QTStr("Importer.Path"); + break; + case ImporterColumn::Program: + result = QTStr("Importer.Program"); + break; + case ImporterColumn::Name: + result = QTStr("Name"); + } + } + + return result; +} + +/** + Window +**/ + +OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(optionsModel); + ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); + + connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); + + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); + ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, + &OBSImporter::importCollections); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); + + ImportersInit(); + + bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); + + if (!autoSearchPrompt) { + QMessageBox::StandardButton button = OBSMessageBox::question( + parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); + + if (button == QMessageBox::Yes) { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); + } else { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); + } + + config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); + } + + bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + + OBSImporterFiles f; + if (autoSearch) + f = ImportersFindFiles(); + + for (size_t i = 0; i < f.size(); i++) { + QString path = f[i].c_str(); + path.replace("\\", "/"); + addImportOption(path, true); + } + + f.clear(); + + ui->tableView->resizeColumnsToContents(); + + QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +void OBSImporter::addImportOption(QString path, bool automatic) +{ + QStringList list; + + list.append(path); + + QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); + + optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); +} + +void OBSImporter::dropEvent(QDropEvent *ev) +{ + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + if (fileInfo.isDir()) { + + QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); + + while (dirIter.hasNext()) { + addImportOption(dirIter.next(), false); + } + } else { + addImportOption(fileInfo.canonicalFilePath(), false); + } + } +} + +void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls()) + ev->accept(); +} + +void OBSImporter::browseImport() +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + for (int i = 0; i < paths.count(); i++) { + addImportOption(paths[i], false); + } + } +} + +bool GetUnusedName(std::string &name) +{ + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { + return false; + } + + std::string newName; + int inc = 2; + do { + newName = name; + newName += " "; + newName += std::to_string(inc++); + } while (basic->GetSceneCollectionByName(newName)); + + name = newName; + return true; +} + +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + +void OBSImporter::importCollections() +{ + setEnabled(false); + + const std::filesystem::path sceneCollectionLocation = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); + + for (int i = 0; i < optionsModel->rowCount() - 1; i++) { + int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); + + if (selected == Qt::Unchecked) + continue; + + std::string pathStr = optionsModel->index(i, ImporterColumn::Path) + .data(Qt::DisplayRole) + .value() + .toStdString(); + std::string nameStr = optionsModel->index(i, ImporterColumn::Name) + .data(Qt::DisplayRole) + .value() + .toStdString(); + + json11::Json res; + ImportSC(pathStr, nameStr, res); + + if (res != json11::Json()) { + json11::Json::object out = res.object_items(); + std::string name = res["name"].string_value(); + std::string file; + + if (GetUnusedName(name)) { + json11::Json::object newOut = out; + newOut["name"] = name; + out = newOut; + } + + std::string fileName; + if (!GetFileSafeName(name.c_str(), fileName)) { + blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); + } + + std::string collectionFile; + collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); + collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); + } + + std::string out_str = json11::Json(out).dump(); + + bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), + false); + + blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), + success ? "SUCCESS" : "FAILURE"); + } + } + + close(); +} + +void OBSImporter::dataChanged() +{ + ui->tableView->resizeColumnToContents(ImporterColumn::Name); +} diff --git a/frontend/importer/OBSImporter.hpp b/frontend/importer/OBSImporter.hpp new file mode 100644 index 000000000..8e77db645 --- /dev/null +++ b/frontend/importer/OBSImporter.hpp @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + 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 "obs-app.hpp" +#include "window-basic-main.hpp" +#include +#include +#include +#include "ui_OBSImporter.h" + +class ImporterModel; + +class OBSImporter : public QDialog { + Q_OBJECT + + QPointer optionsModel; + std::unique_ptr ui; + +public: + explicit OBSImporter(QWidget *parent = nullptr); + + void addImportOption(QString path, bool automatic); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + +public slots: + void browseImport(); + void importCollections(); + void dataChanged(); +}; + +class ImporterModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSImporter; + +public: + ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + +private: + struct ImporterEntry { + QString path; + QString program; + QString name; + + bool selected; + bool empty; + }; + + QList options; + + void checkInputPath(int row); +}; + +class ImporterEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + ImporterEntryPathItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/UI/importers/classic.cpp b/frontend/importers/classic.cpp similarity index 100% rename from UI/importers/classic.cpp rename to frontend/importers/classic.cpp diff --git a/UI/importers/importers.cpp b/frontend/importers/importers.cpp similarity index 100% rename from UI/importers/importers.cpp rename to frontend/importers/importers.cpp diff --git a/UI/importers/importers.hpp b/frontend/importers/importers.hpp similarity index 100% rename from UI/importers/importers.hpp rename to frontend/importers/importers.hpp diff --git a/UI/importers/sl.cpp b/frontend/importers/sl.cpp similarity index 100% rename from UI/importers/sl.cpp rename to frontend/importers/sl.cpp diff --git a/UI/importers/studio.cpp b/frontend/importers/studio.cpp similarity index 100% rename from UI/importers/studio.cpp rename to frontend/importers/studio.cpp diff --git a/UI/importers/xsplit.cpp b/frontend/importers/xsplit.cpp similarity index 100% rename from UI/importers/xsplit.cpp rename to frontend/importers/xsplit.cpp From 09ebf7ddfc6c361c052e6019d797b65b3d525828 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 20:38:37 +0100 Subject: [PATCH 09/37] frontend: Split OBSImporter into single files per C++ class --- .../ImporterEntryPathItemDelegate.cpp | 424 +----------------- .../ImporterEntryPathItemDelegate.hpp | 58 --- frontend/importer/ImporterModel.cpp | 372 +-------------- frontend/importer/ImporterModel.hpp | 59 +-- frontend/importer/OBSImporter.cpp | 360 +-------------- frontend/importer/OBSImporter.hpp | 62 +-- 6 files changed, 34 insertions(+), 1301 deletions(-) diff --git a/frontend/importer/ImporterEntryPathItemDelegate.cpp b/frontend/importer/ImporterEntryPathItemDelegate.cpp index 88d4ad2d9..d6b3b8f7c 100644 --- a/frontend/importer/ImporterEntryPathItemDelegate.cpp +++ b/frontend/importer/ImporterEntryPathItemDelegate.cpp @@ -15,37 +15,18 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-importer.cpp" +#include "ImporterEntryPathItemDelegate.hpp" +#include "ImporterModel.hpp" -#include "obs-app.hpp" +#include -#include -#include -#include -#include -#include -#include -#include #include -#include "importers/importers.hpp" +#include +#include +#include -extern bool SceneCollectionExists(const char *findName); - -enum ImporterColumn { - Selected, - Name, - Path, - Program, - - Count -}; - -enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ +#include "moc_ImporterEntryPathItemDelegate.cpp" ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} @@ -177,394 +158,3 @@ void ImporterEntryPathItemDelegate::updateText() QWidget *editor = lineEdit->parentWidget(); emit commitData(editor); } - -/** - Model -**/ - -int ImporterModel::rowCount(const QModelIndex &) const -{ - return options.length() + 1; -} - -int ImporterModel::columnCount(const QModelIndex &) const -{ - return ImporterColumn::Count; -} - -QVariant ImporterModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= options.length()) { - if (role == ImporterEntryRole::CheckEmpty) - result = true; - else - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case ImporterColumn::Path: - result = options[index.row()].path; - break; - case ImporterColumn::Program: - result = options[index.row()].program; - break; - case ImporterColumn::Name: - result = options[index.row()].name; - } - } else if (role == Qt::EditRole) { - if (index.column() == ImporterColumn::Name) { - result = options[index.row()].name; - } - } else if (role == Qt::CheckStateRole) { - switch (index.column()) { - case ImporterColumn::Selected: - if (options[index.row()].program != "") - result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; - else - result = Qt::Unchecked; - } - } else if (role == ImporterEntryRole::CheckEmpty) { - result = options[index.row()].empty; - } - - return result; -} - -Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { - flags |= Qt::ItemIsUserCheckable; - } else if (index.column() == ImporterColumn::Path || - (index.column() == ImporterColumn::Name && index.row() != options.length())) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void ImporterModel::checkInputPath(int row) -{ - ImporterEntry &entry = options[row]; - - if (entry.path.isEmpty()) { - entry.program = ""; - entry.empty = true; - entry.selected = false; - entry.name = ""; - } else { - entry.empty = false; - - std::string program = DetectProgram(entry.path.toStdString()); - entry.program = QTStr(program.c_str()); - - if (program.empty()) { - entry.selected = false; - } else { - std::string name = GetSCName(entry.path.toStdString(), program); - entry.name = name.c_str(); - } - } - - emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); -} - -bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (role == ImporterEntryRole::NewPath) { - QStringList list = value.toStringList(); - - if (list.size() == 0) { - if (index.row() < options.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - options.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (list.size() > 0 && index.row() < options.length()) { - options[index.row()].path = list[0]; - checkInputPath(index.row()); - - list.removeAt(0); - } - - if (list.size() > 0) { - int row = index.row(); - int lastRow = row + list.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : list) { - ImporterEntry entry; - entry.path = path; - - options.insert(row, entry); - - row++; - } - - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - } - } - } else if (index.row() == options.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - ImporterEntry entry; - entry.path = path; - entry.selected = role != ImporterEntryRole::AutoPath; - entry.empty = false; - - beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); - options.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - } - } else if (index.column() == ImporterColumn::Selected) { - bool select = value.toBool(); - - options[index.row()].selected = select; - } else if (index.column() == ImporterColumn::Path) { - QString path = value.toString(); - options[index.row()].path = path; - - checkInputPath(index.row()); - } else if (index.column() == ImporterColumn::Name) { - QString name = value.toString(); - options[index.row()].name = name; - } - - emit dataChanged(index, index); - - return true; -} - -QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case ImporterColumn::Path: - result = QTStr("Importer.Path"); - break; - case ImporterColumn::Program: - result = QTStr("Importer.Program"); - break; - case ImporterColumn::Name: - result = QTStr("Name"); - } - } - - return result; -} - -/** - Window -**/ - -OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(optionsModel); - ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); - - connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); - - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); - ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, - &OBSImporter::importCollections); - connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); - - ImportersInit(); - - bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); - - if (!autoSearchPrompt) { - QMessageBox::StandardButton button = OBSMessageBox::question( - parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); - - if (button == QMessageBox::Yes) { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); - } else { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); - } - - config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); - } - - bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); - - OBSImporterFiles f; - if (autoSearch) - f = ImportersFindFiles(); - - for (size_t i = 0; i < f.size(); i++) { - QString path = f[i].c_str(); - path.replace("\\", "/"); - addImportOption(path, true); - } - - f.clear(); - - ui->tableView->resizeColumnsToContents(); - - QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -void OBSImporter::addImportOption(QString path, bool automatic) -{ - QStringList list; - - list.append(path); - - QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); - - optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); -} - -void OBSImporter::dropEvent(QDropEvent *ev) -{ - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - if (fileInfo.isDir()) { - - QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); - - while (dirIter.hasNext()) { - addImportOption(dirIter.next(), false); - } - } else { - addImportOption(fileInfo.canonicalFilePath(), false); - } - } -} - -void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls()) - ev->accept(); -} - -void OBSImporter::browseImport() -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - for (int i = 0; i < paths.count(); i++) { - addImportOption(paths[i], false); - } - } -} - -bool GetUnusedName(std::string &name) -{ - OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - if (!basic->GetSceneCollectionByName(name)) { - return false; - } - - std::string newName; - int inc = 2; - do { - newName = name; - newName += " "; - newName += std::to_string(inc++); - } while (basic->GetSceneCollectionByName(newName)); - - name = newName; - return true; -} - -constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; - -void OBSImporter::importCollections() -{ - setEnabled(false); - - const std::filesystem::path sceneCollectionLocation = - App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); - - for (int i = 0; i < optionsModel->rowCount() - 1; i++) { - int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); - - if (selected == Qt::Unchecked) - continue; - - std::string pathStr = optionsModel->index(i, ImporterColumn::Path) - .data(Qt::DisplayRole) - .value() - .toStdString(); - std::string nameStr = optionsModel->index(i, ImporterColumn::Name) - .data(Qt::DisplayRole) - .value() - .toStdString(); - - json11::Json res; - ImportSC(pathStr, nameStr, res); - - if (res != json11::Json()) { - json11::Json::object out = res.object_items(); - std::string name = res["name"].string_value(); - std::string file; - - if (GetUnusedName(name)) { - json11::Json::object newOut = out; - newOut["name"] = name; - out = newOut; - } - - std::string fileName; - if (!GetFileSafeName(name.c_str(), fileName)) { - blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); - } - - std::string collectionFile; - collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); - collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); - - if (!GetClosestUnusedFileName(collectionFile, "json")) { - blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); - } - - std::string out_str = json11::Json(out).dump(); - - bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), - false); - - blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), - success ? "SUCCESS" : "FAILURE"); - } - } - - close(); -} - -void OBSImporter::dataChanged() -{ - ui->tableView->resizeColumnToContents(ImporterColumn::Name); -} diff --git a/frontend/importer/ImporterEntryPathItemDelegate.hpp b/frontend/importer/ImporterEntryPathItemDelegate.hpp index 8e77db645..61304c9cf 100644 --- a/frontend/importer/ImporterEntryPathItemDelegate.hpp +++ b/frontend/importer/ImporterEntryPathItemDelegate.hpp @@ -17,65 +17,7 @@ #pragma once -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include #include -#include -#include "ui_OBSImporter.h" - -class ImporterModel; - -class OBSImporter : public QDialog { - Q_OBJECT - - QPointer optionsModel; - std::unique_ptr ui; - -public: - explicit OBSImporter(QWidget *parent = nullptr); - - void addImportOption(QString path, bool automatic); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - -public slots: - void browseImport(); - void importCollections(); - void dataChanged(); -}; - -class ImporterModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSImporter; - -public: - ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - -private: - struct ImporterEntry { - QString path; - QString program; - QString name; - - bool selected; - bool empty; - }; - - QList options; - - void checkInputPath(int row); -}; class ImporterEntryPathItemDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/importer/ImporterModel.cpp b/frontend/importer/ImporterModel.cpp index 88d4ad2d9..0c6e22deb 100644 --- a/frontend/importer/ImporterModel.cpp +++ b/frontend/importer/ImporterModel.cpp @@ -15,172 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-importer.cpp" +#include "ImporterModel.hpp" -#include "obs-app.hpp" +#include +#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "importers/importers.hpp" - -extern bool SceneCollectionExists(const char *findName); - -enum ImporterColumn { - Selected, - Name, - Path, - Program, - - Count -}; - -enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} - -QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - bool empty = index.model() - ->index(index.row(), ImporterColumn::Path) - .data(ImporterEntryRole::CheckEmpty) - .value(); - - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; -} - -void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - model->setData(index, list, ImporterEntryRole::NewPath); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - - bool isSet = false; - QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } - - if (isSet) - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/** - Model -**/ +#include "moc_ImporterModel.cpp" int ImporterModel::rowCount(const QModelIndex &) const { @@ -364,207 +204,3 @@ QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int return result; } - -/** - Window -**/ - -OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(optionsModel); - ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); - - connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); - - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); - ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, - &OBSImporter::importCollections); - connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); - - ImportersInit(); - - bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); - - if (!autoSearchPrompt) { - QMessageBox::StandardButton button = OBSMessageBox::question( - parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); - - if (button == QMessageBox::Yes) { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); - } else { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); - } - - config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); - } - - bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); - - OBSImporterFiles f; - if (autoSearch) - f = ImportersFindFiles(); - - for (size_t i = 0; i < f.size(); i++) { - QString path = f[i].c_str(); - path.replace("\\", "/"); - addImportOption(path, true); - } - - f.clear(); - - ui->tableView->resizeColumnsToContents(); - - QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -void OBSImporter::addImportOption(QString path, bool automatic) -{ - QStringList list; - - list.append(path); - - QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); - - optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); -} - -void OBSImporter::dropEvent(QDropEvent *ev) -{ - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - if (fileInfo.isDir()) { - - QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); - - while (dirIter.hasNext()) { - addImportOption(dirIter.next(), false); - } - } else { - addImportOption(fileInfo.canonicalFilePath(), false); - } - } -} - -void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls()) - ev->accept(); -} - -void OBSImporter::browseImport() -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - for (int i = 0; i < paths.count(); i++) { - addImportOption(paths[i], false); - } - } -} - -bool GetUnusedName(std::string &name) -{ - OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - if (!basic->GetSceneCollectionByName(name)) { - return false; - } - - std::string newName; - int inc = 2; - do { - newName = name; - newName += " "; - newName += std::to_string(inc++); - } while (basic->GetSceneCollectionByName(newName)); - - name = newName; - return true; -} - -constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; - -void OBSImporter::importCollections() -{ - setEnabled(false); - - const std::filesystem::path sceneCollectionLocation = - App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); - - for (int i = 0; i < optionsModel->rowCount() - 1; i++) { - int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); - - if (selected == Qt::Unchecked) - continue; - - std::string pathStr = optionsModel->index(i, ImporterColumn::Path) - .data(Qt::DisplayRole) - .value() - .toStdString(); - std::string nameStr = optionsModel->index(i, ImporterColumn::Name) - .data(Qt::DisplayRole) - .value() - .toStdString(); - - json11::Json res; - ImportSC(pathStr, nameStr, res); - - if (res != json11::Json()) { - json11::Json::object out = res.object_items(); - std::string name = res["name"].string_value(); - std::string file; - - if (GetUnusedName(name)) { - json11::Json::object newOut = out; - newOut["name"] = name; - out = newOut; - } - - std::string fileName; - if (!GetFileSafeName(name.c_str(), fileName)) { - blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); - } - - std::string collectionFile; - collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); - collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); - - if (!GetClosestUnusedFileName(collectionFile, "json")) { - blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); - } - - std::string out_str = json11::Json(out).dump(); - - bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), - false); - - blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), - success ? "SUCCESS" : "FAILURE"); - } - } - - close(); -} - -void OBSImporter::dataChanged() -{ - ui->tableView->resizeColumnToContents(ImporterColumn::Name); -} diff --git a/frontend/importer/ImporterModel.hpp b/frontend/importer/ImporterModel.hpp index 8e77db645..011ac83eb 100644 --- a/frontend/importer/ImporterModel.hpp +++ b/frontend/importer/ImporterModel.hpp @@ -17,36 +17,19 @@ #pragma once -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include -#include -#include -#include "ui_OBSImporter.h" +#include -class ImporterModel; +enum ImporterColumn { + Selected, + Name, + Path, + Program, -class OBSImporter : public QDialog { - Q_OBJECT - - QPointer optionsModel; - std::unique_ptr ui; - -public: - explicit OBSImporter(QWidget *parent = nullptr); - - void addImportOption(QString path, bool automatic); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - -public slots: - void browseImport(); - void importCollections(); - void dataChanged(); + Count }; +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + class ImporterModel : public QAbstractTableModel { Q_OBJECT @@ -76,27 +59,3 @@ private: void checkInputPath(int row); }; - -class ImporterEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - ImporterEntryPathItemDelegate(); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; diff --git a/frontend/importer/OBSImporter.cpp b/frontend/importer/OBSImporter.cpp index 88d4ad2d9..c33a095c6 100644 --- a/frontend/importer/OBSImporter.cpp +++ b/frontend/importer/OBSImporter.cpp @@ -15,360 +15,22 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-importer.cpp" +#include "OBSImporter.hpp" +#include "ImporterEntryPathItemDelegate.hpp" +#include "ImporterModel.hpp" -#include "obs-app.hpp" +#include +#include -#include -#include -#include -#include -#include -#include -#include #include -#include "importers/importers.hpp" - -extern bool SceneCollectionExists(const char *findName); - -enum ImporterColumn { - Selected, - Name, - Path, - Program, - - Count -}; - -enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} - -QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - bool empty = index.model() - ->index(index.row(), ImporterColumn::Path) - .data(ImporterEntryRole::CheckEmpty) - .value(); - - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; -} - -void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - model->setData(index, list, ImporterEntryRole::NewPath); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - - bool isSet = false; - QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } - - if (isSet) - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/** - Model -**/ - -int ImporterModel::rowCount(const QModelIndex &) const -{ - return options.length() + 1; -} - -int ImporterModel::columnCount(const QModelIndex &) const -{ - return ImporterColumn::Count; -} - -QVariant ImporterModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= options.length()) { - if (role == ImporterEntryRole::CheckEmpty) - result = true; - else - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case ImporterColumn::Path: - result = options[index.row()].path; - break; - case ImporterColumn::Program: - result = options[index.row()].program; - break; - case ImporterColumn::Name: - result = options[index.row()].name; - } - } else if (role == Qt::EditRole) { - if (index.column() == ImporterColumn::Name) { - result = options[index.row()].name; - } - } else if (role == Qt::CheckStateRole) { - switch (index.column()) { - case ImporterColumn::Selected: - if (options[index.row()].program != "") - result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; - else - result = Qt::Unchecked; - } - } else if (role == ImporterEntryRole::CheckEmpty) { - result = options[index.row()].empty; - } - - return result; -} - -Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { - flags |= Qt::ItemIsUserCheckable; - } else if (index.column() == ImporterColumn::Path || - (index.column() == ImporterColumn::Name && index.row() != options.length())) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void ImporterModel::checkInputPath(int row) -{ - ImporterEntry &entry = options[row]; - - if (entry.path.isEmpty()) { - entry.program = ""; - entry.empty = true; - entry.selected = false; - entry.name = ""; - } else { - entry.empty = false; - - std::string program = DetectProgram(entry.path.toStdString()); - entry.program = QTStr(program.c_str()); - - if (program.empty()) { - entry.selected = false; - } else { - std::string name = GetSCName(entry.path.toStdString(), program); - entry.name = name.c_str(); - } - } - - emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); -} - -bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (role == ImporterEntryRole::NewPath) { - QStringList list = value.toStringList(); - - if (list.size() == 0) { - if (index.row() < options.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - options.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (list.size() > 0 && index.row() < options.length()) { - options[index.row()].path = list[0]; - checkInputPath(index.row()); - - list.removeAt(0); - } - - if (list.size() > 0) { - int row = index.row(); - int lastRow = row + list.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : list) { - ImporterEntry entry; - entry.path = path; - - options.insert(row, entry); - - row++; - } - - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - } - } - } else if (index.row() == options.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - ImporterEntry entry; - entry.path = path; - entry.selected = role != ImporterEntryRole::AutoPath; - entry.empty = false; - - beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); - options.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - } - } else if (index.column() == ImporterColumn::Selected) { - bool select = value.toBool(); - - options[index.row()].selected = select; - } else if (index.column() == ImporterColumn::Path) { - QString path = value.toString(); - options[index.row()].path = path; - - checkInputPath(index.row()); - } else if (index.column() == ImporterColumn::Name) { - QString name = value.toString(); - options[index.row()].name = name; - } - - emit dataChanged(index, index); - - return true; -} - -QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case ImporterColumn::Path: - result = QTStr("Importer.Path"); - break; - case ImporterColumn::Program: - result = QTStr("Importer.Program"); - break; - case ImporterColumn::Name: - result = QTStr("Name"); - } - } - - return result; -} - -/** - Window -**/ +#include +#include +#include +#include +#include +#include "moc_OBSImporter.cpp" OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) { setAcceptDrops(true); diff --git a/frontend/importer/OBSImporter.hpp b/frontend/importer/OBSImporter.hpp index 8e77db645..d9ab2bb6f 100644 --- a/frontend/importer/OBSImporter.hpp +++ b/frontend/importer/OBSImporter.hpp @@ -17,13 +17,11 @@ #pragma once -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include -#include -#include #include "ui_OBSImporter.h" +#include +#include + class ImporterModel; class OBSImporter : public QDialog { @@ -46,57 +44,3 @@ public slots: void importCollections(); void dataChanged(); }; - -class ImporterModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSImporter; - -public: - ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - -private: - struct ImporterEntry { - QString path; - QString program; - QString name; - - bool selected; - bool empty; - }; - - QList options; - - void checkInputPath(int row); -}; - -class ImporterEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - ImporterEntryPathItemDelegate(); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; From 8e4ee1d3fc43ee2c2bbaab6c4224a769cda7c3fc Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 20:41:22 +0100 Subject: [PATCH 10/37] frontend: Add renamed OAuth implementation --- UI/auth-base.cpp => frontend/oauth/Auth.cpp | 8 +++---- UI/auth-base.hpp => frontend/oauth/Auth.hpp | 2 -- .../oauth/AuthListener.cpp | 14 ++++++----- .../oauth/AuthListener.hpp | 3 ++- .../oauth/RestreamAuth.cpp | 24 +++++++++---------- .../oauth/RestreamAuth.hpp | 4 +--- .../oauth/TwitchAuth.cpp | 23 +++++++----------- .../oauth/TwitchAuth.hpp | 8 ++----- 8 files changed, 37 insertions(+), 49 deletions(-) rename UI/auth-base.cpp => frontend/oauth/Auth.cpp (94%) rename UI/auth-base.hpp => frontend/oauth/Auth.hpp (96%) rename UI/auth-listener.cpp => frontend/oauth/AuthListener.cpp (95%) rename UI/auth-listener.hpp => frontend/oauth/AuthListener.hpp (90%) rename UI/auth-restream.cpp => frontend/oauth/RestreamAuth.cpp (96%) rename UI/auth-restream.hpp => frontend/oauth/RestreamAuth.hpp (90%) rename UI/auth-twitch.cpp => frontend/oauth/TwitchAuth.cpp (97%) rename UI/auth-twitch.hpp => frontend/oauth/TwitchAuth.hpp (86%) diff --git a/UI/auth-base.cpp b/frontend/oauth/Auth.cpp similarity index 94% rename from UI/auth-base.cpp rename to frontend/oauth/Auth.cpp index 1f0999fe0..89b579f8e 100644 --- a/UI/auth-base.cpp +++ b/frontend/oauth/Auth.cpp @@ -1,8 +1,8 @@ -#include "moc_auth-base.cpp" -#include "window-basic-main.hpp" +#include "Auth.hpp" -#include -#include +#include + +#include "moc_Auth.cpp" struct AuthInfo { Auth::Def def; diff --git a/UI/auth-base.hpp b/frontend/oauth/Auth.hpp similarity index 96% rename from UI/auth-base.hpp rename to frontend/oauth/Auth.hpp index 9745f63fe..c983c5463 100644 --- a/UI/auth-base.hpp +++ b/frontend/oauth/Auth.hpp @@ -1,8 +1,6 @@ #pragma once #include -#include -#include class Auth : public QObject { Q_OBJECT diff --git a/UI/auth-listener.cpp b/frontend/oauth/AuthListener.cpp similarity index 95% rename from UI/auth-listener.cpp rename to frontend/oauth/AuthListener.cpp index e652da4d3..9298a18e5 100644 --- a/UI/auth-listener.cpp +++ b/frontend/oauth/AuthListener.cpp @@ -1,12 +1,14 @@ -#include "moc_auth-listener.cpp" +#include "AuthListener.hpp" + +#include -#include -#include -#include -#include #include -#include "obs-app.hpp" +#include +#include +#include + +#include "moc_AuthListener.cpp" #define LOGO_URL "https://obsproject.com/assets/images/new_icon_small-r.png" diff --git a/UI/auth-listener.hpp b/frontend/oauth/AuthListener.hpp similarity index 90% rename from UI/auth-listener.hpp rename to frontend/oauth/AuthListener.hpp index fa1db2dea..65bcf540e 100644 --- a/UI/auth-listener.hpp +++ b/frontend/oauth/AuthListener.hpp @@ -1,7 +1,8 @@ #pragma once #include -#include + +class QTcpServer; class AuthListener : public QObject { Q_OBJECT diff --git a/UI/auth-restream.cpp b/frontend/oauth/RestreamAuth.cpp similarity index 96% rename from UI/auth-restream.cpp rename to frontend/oauth/RestreamAuth.cpp index c832ee83d..e428e889d 100644 --- a/UI/auth-restream.cpp +++ b/frontend/oauth/RestreamAuth.cpp @@ -1,19 +1,17 @@ -#include "moc_auth-restream.cpp" +#include "RestreamAuth.hpp" + +#include +#include +#include +#include +#include -#include -#include -#include #include -#include -#include -#include +#include -#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" +#include + +#include "moc_RestreamAuth.cpp" using namespace json11; diff --git a/UI/auth-restream.hpp b/frontend/oauth/RestreamAuth.hpp similarity index 90% rename from UI/auth-restream.hpp rename to frontend/oauth/RestreamAuth.hpp index 16c77e813..6ea9128a1 100644 --- a/UI/auth-restream.hpp +++ b/frontend/oauth/RestreamAuth.hpp @@ -1,8 +1,6 @@ #pragma once -#include "auth-oauth.hpp" - -class BrowserDock; +#include "OAuth.hpp" class RestreamAuth : public OAuthStreamKey { Q_OBJECT diff --git a/UI/auth-twitch.cpp b/frontend/oauth/TwitchAuth.cpp similarity index 97% rename from UI/auth-twitch.cpp rename to frontend/oauth/TwitchAuth.cpp index 40f70b567..c6852e29d 100644 --- a/UI/auth-twitch.cpp +++ b/frontend/oauth/TwitchAuth.cpp @@ -1,22 +1,17 @@ -#include "moc_auth-twitch.cpp" +#include "TwitchAuth.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include -#include +#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" +#include -#include - -#include "ui-config.h" -#include "obf.h" +#include "moc_TwitchAuth.cpp" using namespace json11; diff --git a/UI/auth-twitch.hpp b/frontend/oauth/TwitchAuth.hpp similarity index 86% rename from UI/auth-twitch.hpp rename to frontend/oauth/TwitchAuth.hpp index 9fb3ba773..bbd8ca4b9 100644 --- a/UI/auth-twitch.hpp +++ b/frontend/oauth/TwitchAuth.hpp @@ -1,14 +1,10 @@ #pragma once -#include -#include -#include -#include +#include "OAuth.hpp" #include -#include "auth-oauth.hpp" -class BrowserDock; +#include class TwitchAuth : public OAuthStreamKey { Q_OBJECT From f80b591a72ae441a9b05debaa0e818d33f5133dd Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Thu, 21 Nov 2024 20:55:13 +0100 Subject: [PATCH 11/37] frontend: Prepare OAuth implementation for splits --- .../dialogs/OAuthLogin.cpp | 0 .../dialogs/OAuthLogin.hpp | 0 .../docks/YouTubeChatDock.cpp | 0 .../docks/YouTubeChatDock.hpp | 0 frontend/oauth/OAuth.cpp | 322 ++++++++++++++++ frontend/oauth/OAuth.hpp | 82 +++++ frontend/oauth/YoutubeAuth.cpp | 343 ++++++++++++++++++ frontend/oauth/YoutubeAuth.hpp | 62 ++++ 8 files changed, 809 insertions(+) rename UI/auth-oauth.cpp => frontend/dialogs/OAuthLogin.cpp (100%) rename UI/auth-oauth.hpp => frontend/dialogs/OAuthLogin.hpp (100%) rename UI/auth-youtube.cpp => frontend/docks/YouTubeChatDock.cpp (100%) rename UI/auth-youtube.hpp => frontend/docks/YouTubeChatDock.hpp (100%) create mode 100644 frontend/oauth/OAuth.cpp create mode 100644 frontend/oauth/OAuth.hpp create mode 100644 frontend/oauth/YoutubeAuth.cpp create mode 100644 frontend/oauth/YoutubeAuth.hpp diff --git a/UI/auth-oauth.cpp b/frontend/dialogs/OAuthLogin.cpp similarity index 100% rename from UI/auth-oauth.cpp rename to frontend/dialogs/OAuthLogin.cpp diff --git a/UI/auth-oauth.hpp b/frontend/dialogs/OAuthLogin.hpp similarity index 100% rename from UI/auth-oauth.hpp rename to frontend/dialogs/OAuthLogin.hpp diff --git a/UI/auth-youtube.cpp b/frontend/docks/YouTubeChatDock.cpp similarity index 100% rename from UI/auth-youtube.cpp rename to frontend/docks/YouTubeChatDock.cpp diff --git a/UI/auth-youtube.hpp b/frontend/docks/YouTubeChatDock.hpp similarity index 100% rename from UI/auth-youtube.hpp rename to frontend/docks/YouTubeChatDock.hpp diff --git a/frontend/oauth/OAuth.cpp b/frontend/oauth/OAuth.cpp new file mode 100644 index 000000000..04458c9c5 --- /dev/null +++ b/frontend/oauth/OAuth.cpp @@ -0,0 +1,322 @@ +#include "moc_auth-oauth.cpp" + +#include +#include +#include + +#include +#include + +#include "window-basic-main.hpp" +#include "remote-text.hpp" + +#include + +#include + +#include "ui-config.h" + +using namespace json11; + +#ifdef BROWSER_AVAILABLE +#include +extern QCef *cef; +extern QCefCookieManager *panel_cookies; +#endif + +/* ------------------------------------------------------------------------- */ + +OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) +{ +#ifdef BROWSER_AVAILABLE + if (!cef) { + return; + } + + setWindowTitle("Auth"); + setMinimumSize(400, 400); + resize(700, 700); + + Qt::WindowFlags flags = windowFlags(); + Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; + setWindowFlags(flags & (~helpFlag)); + + OBSBasic::InitBrowserPanelSafeBlock(); + + cefWidget = cef->create_widget(nullptr, url, panel_cookies); + if (!cefWidget) { + fail = true; + return; + } + + connect(cefWidget, &QCefWidget::titleChanged, this, &OAuthLogin::setWindowTitle); + connect(cefWidget, &QCefWidget::urlChanged, this, &OAuthLogin::urlChanged); + + QPushButton *close = new QPushButton(QTStr("Cancel")); + connect(close, &QAbstractButton::clicked, this, &QDialog::reject); + + QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->addStretch(); + bottomLayout->addWidget(close); + bottomLayout->addStretch(); + + QVBoxLayout *topLayout = new QVBoxLayout(this); + topLayout->addWidget(cefWidget); + topLayout->addLayout(bottomLayout); +#else + UNUSED_PARAMETER(url); +#endif +} + +OAuthLogin::~OAuthLogin() {} + +int OAuthLogin::exec() +{ +#ifdef BROWSER_AVAILABLE + if (cefWidget) { + return QDialog::exec(); + } +#endif + return QDialog::Rejected; +} + +void OAuthLogin::reject() +{ +#ifdef BROWSER_AVAILABLE + delete cefWidget; +#endif + QDialog::reject(); +} + +void OAuthLogin::accept() +{ +#ifdef BROWSER_AVAILABLE + delete cefWidget; +#endif + QDialog::accept(); +} + +void OAuthLogin::urlChanged(const QString &url) +{ + std::string uri = get_token ? "access_token=" : "code="; + int code_idx = url.indexOf(uri.c_str()); + if (code_idx == -1) + return; + + if (!url.startsWith(OAUTH_BASE_URL)) + return; + + code_idx += (int)uri.size(); + + int next_idx = url.indexOf("&", code_idx); + if (next_idx != -1) + code = url.mid(code_idx, next_idx - code_idx); + else + code = url.right(url.size() - code_idx); + + accept(); +} + +/* ------------------------------------------------------------------------- */ + +struct OAuthInfo { + Auth::Def def; + OAuth::login_cb login; + OAuth::delete_cookies_cb delete_cookies; +}; + +static std::vector loginCBs; + +void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies) +{ + OAuthInfo info = {d, login, delete_cookies}; + loginCBs.push_back(info); + RegisterAuth(d, create); +} + +std::shared_ptr OAuth::Login(QWidget *parent, const std::string &service) +{ + for (auto &a : loginCBs) { + if (service.find(a.def.service) != std::string::npos) { + return a.login(parent, service); + } + } + + return nullptr; +} + +void OAuth::DeleteCookies(const std::string &service) +{ + for (auto &a : loginCBs) { + if (service.find(a.def.service) != std::string::npos) { + a.delete_cookies(); + } + } +} + +void OAuth::SaveInternal() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_string(main->Config(), service(), "RefreshToken", refresh_token.c_str()); + config_set_string(main->Config(), service(), "Token", token.c_str()); + config_set_uint(main->Config(), service(), "ExpireTime", expire_time); + config_set_int(main->Config(), service(), "ScopeVer", currentScopeVer); +} + +static inline std::string get_config_str(OBSBasic *main, const char *section, const char *name) +{ + const char *val = config_get_string(main->Config(), section, name); + return val ? val : ""; +} + +bool OAuth::LoadInternal() +{ + OBSBasic *main = OBSBasic::Get(); + refresh_token = get_config_str(main, service(), "RefreshToken"); + token = get_config_str(main, service(), "Token"); + expire_time = config_get_uint(main->Config(), service(), "ExpireTime"); + currentScopeVer = (int)config_get_int(main->Config(), service(), "ScopeVer"); + return implicit ? !token.empty() : !refresh_token.empty(); +} + +bool OAuth::TokenExpired() +{ + if (token.empty()) + return true; + if ((uint64_t)time(nullptr) > expire_time - 5) + return true; + return false; +} + +bool OAuth::GetToken(const char *url, const std::string &client_id, const std::string &secret, + const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry) +{ + return GetTokenInternal(url, client_id, secret, redirect_uri, scope_ver, auth_code, retry); +} + +bool OAuth::GetToken(const char *url, const std::string &client_id, int scope_ver, const std::string &auth_code, + bool retry) +{ + return GetTokenInternal(url, client_id, {}, {}, scope_ver, auth_code, retry); +} + +bool OAuth::GetTokenInternal(const char *url, const std::string &client_id, const std::string &secret, + const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry) +try { + std::string output; + std::string error; + std::string desc; + + if (currentScopeVer > 0 && currentScopeVer < scope_ver) { + if (RetryLogin()) { + return true; + } else { + QString title = QTStr("Auth.InvalidScope.Title"); + QString text = QTStr("Auth.InvalidScope.Text").arg(service()); + + QMessageBox::warning(OBSBasic::Get(), title, text); + } + } + + if (auth_code.empty() && !TokenExpired()) { + return true; + } + + std::string post_data; + post_data += "action=redirect&client_id="; + post_data += client_id; + if (!secret.empty()) { + post_data += "&client_secret="; + post_data += secret; + } + if (!redirect_uri.empty()) { + post_data += "&redirect_uri="; + post_data += redirect_uri; + } + + if (!auth_code.empty()) { + post_data += "&grant_type=authorization_code&code="; + post_data += auth_code; + } else { + post_data += "&grant_type=refresh_token&refresh_token="; + post_data += refresh_token; + } + + bool success = false; + + auto func = [&]() { + success = GetRemoteFile(url, output, error, nullptr, "application/x-www-form-urlencoded", "", + post_data.c_str(), std::vector(), nullptr, 5); + }; + + ExecThreadedWithoutBlocking(func, QTStr("Auth.Authing.Title"), QTStr("Auth.Authing.Text").arg(service())); + if (!success || output.empty()) + throw ErrorInfo("Failed to get token from remote", error); + + Json json = Json::parse(output, error); + if (!error.empty()) + throw ErrorInfo("Failed to parse json", error); + + /* -------------------------- */ + /* error handling */ + + error = json["error"].string_value(); + if (!retry && error == "invalid_grant") { + if (RetryLogin()) { + return true; + } + } + if (!error.empty()) + throw ErrorInfo(error, json["error_description"].string_value()); + + /* -------------------------- */ + /* success! */ + + expire_time = (uint64_t)time(nullptr) + json["expires_in"].int_value(); + token = json["access_token"].string_value(); + if (token.empty()) + throw ErrorInfo("Failed to get token from remote", error); + + if (!auth_code.empty()) { + refresh_token = json["refresh_token"].string_value(); + if (refresh_token.empty()) + throw ErrorInfo("Failed to get refresh token from " + "remote", + error); + + currentScopeVer = scope_ver; + } + + return true; + +} catch (ErrorInfo &info) { + if (!retry) { + QString title = QTStr("Auth.AuthFailure.Title"); + QString text = QTStr("Auth.AuthFailure.Text").arg(service(), info.message.c_str(), info.error.c_str()); + + QMessageBox::warning(OBSBasic::Get(), title, text); + } + + blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), info.error.c_str()); + return false; +} + +void OAuthStreamKey::OnStreamConfig() +{ + if (key_.empty()) + return; + + OBSBasic *main = OBSBasic::Get(); + obs_service_t *service = main->GetService(); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + + bool bwtest = obs_data_get_bool(settings, "bwtest"); + + if (bwtest && strcmp(this->service(), "Twitch") == 0) + obs_data_set_string(settings, "key", (key_ + "?bandwidthtest=true").c_str()); + else + obs_data_set_string(settings, "key", key_.c_str()); + + obs_service_update(service, settings); +} diff --git a/frontend/oauth/OAuth.hpp b/frontend/oauth/OAuth.hpp new file mode 100644 index 000000000..b6602e815 --- /dev/null +++ b/frontend/oauth/OAuth.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include "auth-base.hpp" + +class QCefWidget; + +class OAuthLogin : public QDialog { + Q_OBJECT + + QCefWidget *cefWidget = nullptr; + QString code; + bool get_token = false; + bool fail = false; + +public: + OAuthLogin(QWidget *parent, const std::string &url, bool token); + ~OAuthLogin(); + + inline QString GetCode() const { return code; } + inline bool LoadFail() const { return fail; } + + virtual int exec() override; + virtual void reject() override; + virtual void accept() override; + +public slots: + void urlChanged(const QString &url); +}; + +class OAuth : public Auth { + Q_OBJECT + +public: + inline OAuth(const Def &d) : Auth(d) {} + + typedef std::function(QWidget *, const std::string &service_name)> login_cb; + typedef std::function delete_cookies_cb; + + static std::shared_ptr Login(QWidget *parent, const std::string &service); + static void DeleteCookies(const std::string &service); + + static void RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies); + +protected: + std::string refresh_token; + std::string token; + bool implicit = false; + uint64_t expire_time = 0; + int currentScopeVer = 0; + + virtual void SaveInternal() override; + virtual bool LoadInternal() override; + + virtual bool RetryLogin() = 0; + bool TokenExpired(); + bool GetToken(const char *url, const std::string &client_id, int scope_ver, + const std::string &auth_code = std::string(), bool retry = false); + bool GetToken(const char *url, const std::string &client_id, const std::string &secret, + const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry); + +private: + bool GetTokenInternal(const char *url, const std::string &client_id, const std::string &secret, + const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry); +}; + +class OAuthStreamKey : public OAuth { + Q_OBJECT + +protected: + std::string key_; + +public: + inline OAuthStreamKey(const Def &d) : OAuth(d) {} + + inline const std::string &key() const { return key_; } + + virtual void OnStreamConfig() override; +}; diff --git a/frontend/oauth/YoutubeAuth.cpp b/frontend/oauth/YoutubeAuth.cpp new file mode 100644 index 000000000..714003145 --- /dev/null +++ b/frontend/oauth/YoutubeAuth.cpp @@ -0,0 +1,343 @@ +#include "moc_auth-youtube.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include + +#pragma comment(lib, "shell32") +#endif + +#include "auth-listener.hpp" +#include "obs-app.hpp" +#include "ui-config.h" +#include "youtube-api-wrappers.hpp" +#include "window-basic-main.hpp" +#include "obf.h" + +#ifdef BROWSER_AVAILABLE +#include "window-dock-browser.hpp" +#endif + +using namespace json11; + +/* ------------------------------------------------------------------------- */ +#define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" +#define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" +#define YOUTUBE_SCOPE_VERSION 1 +#define YOUTUBE_API_STATE_LENGTH 32 +#define SECTION_NAME "YouTube" + +#define YOUTUBE_CHAT_PLACEHOLDER_URL "https://obsproject.com/placeholders/youtube-chat" +#define YOUTUBE_CHAT_POPOUT_URL "https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v=%1" + +#define YOUTUBE_CHAT_DOCK_NAME "ytChat" + +static const char allowedChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +static const int allowedCount = static_cast(sizeof(allowedChars) - 1); +/* ------------------------------------------------------------------------- */ + +static inline void OpenBrowser(const QString auth_uri) +{ + QUrl url(auth_uri, QUrl::StrictMode); + QDesktopServices::openUrl(url); +} + +static void DeleteCookies() +{ + if (panel_cookies) { + panel_cookies->DeleteCookies("youtube.com", ""); + panel_cookies->DeleteCookies("google.com", ""); + } +} + +void RegisterYoutubeAuth() +{ + for (auto &service : youtubeServices) { + OAuth::RegisterOAuth( + service, [service]() { return std::make_shared(service); }, + YoutubeAuth::Login, DeleteCookies); + } +} + +YoutubeAuth::YoutubeAuth(const Def &d) : OAuthStreamKey(d), section(SECTION_NAME) {} + +YoutubeAuth::~YoutubeAuth() +{ + if (!uiLoaded) + return; + +#ifdef BROWSER_AVAILABLE + OBSBasic *main = OBSBasic::Get(); + + main->RemoveDockWidget(YOUTUBE_CHAT_DOCK_NAME); + chat = nullptr; +#endif +} + +bool YoutubeAuth::RetryLogin() +{ + return true; +} + +void YoutubeAuth::SaveInternal() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_string(main->Config(), service(), "DockState", main->saveState().toBase64().constData()); + + const char *section_name = section.c_str(); + config_set_string(main->Config(), section_name, "RefreshToken", refresh_token.c_str()); + config_set_string(main->Config(), section_name, "Token", token.c_str()); + config_set_uint(main->Config(), section_name, "ExpireTime", expire_time); + config_set_int(main->Config(), section_name, "ScopeVer", currentScopeVer); +} + +static inline std::string get_config_str(OBSBasic *main, const char *section, const char *name) +{ + const char *val = config_get_string(main->Config(), section, name); + return val ? val : ""; +} + +bool YoutubeAuth::LoadInternal() +{ + OBSBasic *main = OBSBasic::Get(); + + const char *section_name = section.c_str(); + refresh_token = get_config_str(main, section_name, "RefreshToken"); + token = get_config_str(main, section_name, "Token"); + expire_time = config_get_uint(main->Config(), section_name, "ExpireTime"); + currentScopeVer = (int)config_get_int(main->Config(), section_name, "ScopeVer"); + firstLoad = false; + return implicit ? !token.empty() : !refresh_token.empty(); +} + +void YoutubeAuth::LoadUI() +{ + if (uiLoaded) + return; + +#ifdef BROWSER_AVAILABLE + if (!cef) + return; + + OBSBasic::InitBrowserPanelSafeBlock(); + OBSBasic *main = OBSBasic::Get(); + + QCefWidget *browser; + + QSize size = main->frameSize(); + QPoint pos = main->pos(); + + chat = new YoutubeChatDock(QTStr("Auth.Chat")); + chat->setObjectName(YOUTUBE_CHAT_DOCK_NAME); + chat->resize(300, 600); + chat->setMinimumSize(200, 300); + chat->setAllowedAreas(Qt::AllDockWidgetAreas); + + browser = cef->create_widget(chat, YOUTUBE_CHAT_PLACEHOLDER_URL, panel_cookies); + + chat->SetWidget(browser); + main->AddDockWidget(chat, Qt::RightDockWidgetArea); + + chat->setFloating(true); + chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); + + if (firstLoad) { + chat->setVisible(true); + } +#endif + + main->NewYouTubeAppDock(); + + if (!firstLoad) { + const char *dockStateStr = config_get_string(main->Config(), service(), "DockState"); + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + + if (main->isVisible() || !main->isMaximized()) + main->restoreState(dockState); + } + + uiLoaded = true; +} + +void YoutubeAuth::SetChatId(const QString &chat_id) +{ +#ifdef BROWSER_AVAILABLE + QString chat_url = QString(YOUTUBE_CHAT_POPOUT_URL).arg(chat_id); + + if (chat && chat->cefWidget) { + chat->cefWidget->setURL(chat_url.toStdString()); + } +#else + UNUSED_PARAMETER(chat_id); +#endif +} + +void YoutubeAuth::ResetChat() +{ +#ifdef BROWSER_AVAILABLE + if (chat && chat->cefWidget) { + chat->cefWidget->setURL(YOUTUBE_CHAT_PLACEHOLDER_URL); + } +#endif +} + +void YoutubeAuth::ReloadChat() +{ +#ifdef BROWSER_AVAILABLE + if (chat && chat->cefWidget) { + chat->cefWidget->reloadPage(); + } +#endif +} + +QString YoutubeAuth::GenerateState() +{ + char state[YOUTUBE_API_STATE_LENGTH + 1]; + QRandomGenerator *rng = QRandomGenerator::system(); + int i; + + for (i = 0; i < YOUTUBE_API_STATE_LENGTH; i++) + state[i] = allowedChars[rng->bounded(0, allowedCount)]; + state[i] = 0; + + return state; +} + +// Static. +std::shared_ptr YoutubeAuth::Login(QWidget *owner, const std::string &service) +{ + QString auth_code; + AuthListener server; + + auto it = std::find_if(youtubeServices.begin(), youtubeServices.end(), + [service](auto &item) { return service == item.service; }); + if (it == youtubeServices.end()) { + return nullptr; + } + const auto auth = std::make_shared(*it); + + QString redirect_uri = QString("http://127.0.0.1:%1").arg(server.GetPort()); + + QMessageBox dlg(owner); + dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); + dlg.setWindowTitle(QTStr("YouTube.Auth.WaitingAuth.Title")); + + std::string clientid = YOUTUBE_CLIENTID; + std::string secret = YOUTUBE_SECRET; + deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); + deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); + + QString state; + state = auth->GenerateState(); + server.SetState(state); + + QString url_template; + url_template += "%1"; + url_template += "?response_type=code"; + url_template += "&client_id=%2"; + url_template += "&redirect_uri=%3"; + url_template += "&state=%4"; + url_template += "&scope=https://www.googleapis.com/auth/youtube"; + QString url = url_template.arg(YOUTUBE_AUTH_URL, clientid.c_str(), redirect_uri, state); + + QString text = QTStr("YouTube.Auth.WaitingAuth.Text"); + text = text.arg(QString("Google OAuth Service").arg(url)); + + dlg.setText(text); + dlg.setTextFormat(Qt::RichText); + dlg.setStandardButtons(QMessageBox::StandardButton::Cancel); +#if defined(__APPLE__) && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + /* We can't show clickable links with the native NSAlert, so let's + * force the old non-native dialog instead. */ + dlg.setOption(QMessageBox::Option::DontUseNativeDialog); +#endif + + connect(&dlg, &QMessageBox::buttonClicked, &dlg, [&](QAbstractButton *) { +#ifdef _DEBUG + blog(LOG_DEBUG, "Action Cancelled."); +#endif + // TODO: Stop server. + dlg.reject(); + }); + + // Async Login. + connect(&server, &AuthListener::ok, &dlg, [&dlg, &auth_code](QString code) { +#ifdef _DEBUG + blog(LOG_DEBUG, "Got youtube redirected answer: %s", QT_TO_UTF8(code)); +#endif + auth_code = code; + dlg.accept(); + }); + connect(&server, &AuthListener::fail, &dlg, [&dlg]() { +#ifdef _DEBUG + blog(LOG_DEBUG, "No access granted"); +#endif + dlg.reject(); + }); + + auto open_external_browser = [url]() { + OpenBrowser(url); + }; + QScopedPointer thread(CreateQThread(open_external_browser)); + thread->start(); + +#if defined(__APPLE__) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && QT_VERSION < QT_VERSION_CHECK(6, 6, 0) + const bool nativeDialogs = qApp->testAttribute(Qt::AA_DontUseNativeDialogs); + App()->setAttribute(Qt::AA_DontUseNativeDialogs, true); + dlg.exec(); + App()->setAttribute(Qt::AA_DontUseNativeDialogs, nativeDialogs); +#else + dlg.exec(); +#endif + + if (dlg.result() == QMessageBox::Cancel || dlg.result() == QDialog::Rejected) + return nullptr; + + if (!auth->GetToken(YOUTUBE_TOKEN_URL, clientid, secret, QT_TO_UTF8(redirect_uri), YOUTUBE_SCOPE_VERSION, + QT_TO_UTF8(auth_code), true)) { + return nullptr; + } + + config_t *config = OBSBasic::Get()->Config(); + config_remove_value(config, "YouTube", "ChannelName"); + + ChannelDescription cd; + if (auth->GetChannelDescription(cd)) + config_set_string(config, "YouTube", "ChannelName", QT_TO_UTF8(cd.title)); + + config_save_safe(config, "tmp", nullptr); + return auth; +} + +#ifdef BROWSER_AVAILABLE +void YoutubeChatDock::YoutubeCookieCheck() +{ + QPointer this_ = this; + auto cb = [this_](bool currentlyLoggedIn) { + bool previouslyLoggedIn = this_->isLoggedIn; + this_->isLoggedIn = currentlyLoggedIn; + bool loginStateChanged = (currentlyLoggedIn && !previouslyLoggedIn) || + (!currentlyLoggedIn && previouslyLoggedIn); + if (loginStateChanged) { + OBSBasic *main = OBSBasic::Get(); + if (main->GetYouTubeAppDock() != nullptr) { + QMetaObject::invokeMethod(main->GetYouTubeAppDock(), "SettingsUpdated", + Qt::QueuedConnection, Q_ARG(bool, !currentlyLoggedIn)); + } + } + }; + if (panel_cookies) { + panel_cookies->CheckForCookie("https://www.youtube.com", "SID", cb); + } +} +#endif diff --git a/frontend/oauth/YoutubeAuth.hpp b/frontend/oauth/YoutubeAuth.hpp new file mode 100644 index 000000000..cd560db7d --- /dev/null +++ b/frontend/oauth/YoutubeAuth.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +#include "auth-oauth.hpp" + +#ifdef BROWSER_AVAILABLE +#include "window-dock-browser.hpp" +#include +class YoutubeChatDock : public BrowserDock { + Q_OBJECT + +private: + bool isLoggedIn; + +public: + YoutubeChatDock(const QString &title) : BrowserDock(title) {} + + inline void SetWidget(QCefWidget *widget_) + { + BrowserDock::SetWidget(widget_); + QWidget::connect(cefWidget.get(), &QCefWidget::urlChanged, this, &YoutubeChatDock::YoutubeCookieCheck); + } +private slots: + void YoutubeCookieCheck(); +}; +#endif + +inline const std::vector youtubeServices = {{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, + {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, + {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; + +class YoutubeAuth : public OAuthStreamKey { + Q_OBJECT + + bool uiLoaded = false; + std::string section; + +#ifdef BROWSER_AVAILABLE + YoutubeChatDock *chat = nullptr; +#endif + + virtual bool RetryLogin() override; + virtual void SaveInternal() override; + virtual bool LoadInternal() override; + virtual void LoadUI() override; + + QString GenerateState(); + +public: + YoutubeAuth(const Def &d); + ~YoutubeAuth(); + + void SetChatId(const QString &chat_id); + void ResetChat(); + void ReloadChat(); + + static std::shared_ptr Login(QWidget *parent, const std::string &service); +}; From 75ac9a29d3a4d969e93ae1d625cab4ce3e37c7cb Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 18:44:42 +0100 Subject: [PATCH 12/37] frontend: Split OAuth implementation into single files per C++ class --- frontend/dialogs/OAuthLogin.cpp | 232 +-------------------- frontend/dialogs/OAuthLogin.hpp | 54 ----- frontend/docks/YouTubeChatDock.cpp | 324 +---------------------------- frontend/docks/YouTubeChatDock.hpp | 43 +--- frontend/oauth/OAuth.cpp | 118 +---------- frontend/oauth/OAuth.hpp | 31 +-- frontend/oauth/YoutubeAuth.cpp | 64 ++---- frontend/oauth/YoutubeAuth.hpp | 33 +-- 8 files changed, 42 insertions(+), 857 deletions(-) diff --git a/frontend/dialogs/OAuthLogin.cpp b/frontend/dialogs/OAuthLogin.cpp index 04458c9c5..64af6869b 100644 --- a/frontend/dialogs/OAuthLogin.cpp +++ b/frontend/dialogs/OAuthLogin.cpp @@ -1,31 +1,19 @@ -#include "moc_auth-oauth.cpp" +#include "OAuthLogin.hpp" -#include -#include -#include - -#include -#include - -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include - -#include - -#include "ui-config.h" - -using namespace json11; +#include #ifdef BROWSER_AVAILABLE #include +#endif +#include + +#include "moc_OAuthLogin.cpp" + +#ifdef BROWSER_AVAILABLE extern QCef *cef; extern QCefCookieManager *panel_cookies; #endif -/* ------------------------------------------------------------------------- */ - OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) { #ifdef BROWSER_AVAILABLE @@ -116,207 +104,3 @@ void OAuthLogin::urlChanged(const QString &url) accept(); } - -/* ------------------------------------------------------------------------- */ - -struct OAuthInfo { - Auth::Def def; - OAuth::login_cb login; - OAuth::delete_cookies_cb delete_cookies; -}; - -static std::vector loginCBs; - -void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies) -{ - OAuthInfo info = {d, login, delete_cookies}; - loginCBs.push_back(info); - RegisterAuth(d, create); -} - -std::shared_ptr OAuth::Login(QWidget *parent, const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - return a.login(parent, service); - } - } - - return nullptr; -} - -void OAuth::DeleteCookies(const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - a.delete_cookies(); - } - } -} - -void OAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "RefreshToken", refresh_token.c_str()); - config_set_string(main->Config(), service(), "Token", token.c_str()); - config_set_uint(main->Config(), service(), "ExpireTime", expire_time); - config_set_int(main->Config(), service(), "ScopeVer", currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool OAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - refresh_token = get_config_str(main, service(), "RefreshToken"); - token = get_config_str(main, service(), "Token"); - expire_time = config_get_uint(main->Config(), service(), "ExpireTime"); - currentScopeVer = (int)config_get_int(main->Config(), service(), "ScopeVer"); - return implicit ? !token.empty() : !refresh_token.empty(); -} - -bool OAuth::TokenExpired() -{ - if (token.empty()) - return true; - if ((uint64_t)time(nullptr) > expire_time - 5) - return true; - return false; -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, const std::string &secret, - const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry) -{ - return GetTokenInternal(url, client_id, secret, redirect_uri, scope_ver, auth_code, retry); -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, int scope_ver, const std::string &auth_code, - bool retry) -{ - return GetTokenInternal(url, client_id, {}, {}, scope_ver, auth_code, retry); -} - -bool OAuth::GetTokenInternal(const char *url, const std::string &client_id, const std::string &secret, - const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry) -try { - std::string output; - std::string error; - std::string desc; - - if (currentScopeVer > 0 && currentScopeVer < scope_ver) { - if (RetryLogin()) { - return true; - } else { - QString title = QTStr("Auth.InvalidScope.Title"); - QString text = QTStr("Auth.InvalidScope.Text").arg(service()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - } - - if (auth_code.empty() && !TokenExpired()) { - return true; - } - - std::string post_data; - post_data += "action=redirect&client_id="; - post_data += client_id; - if (!secret.empty()) { - post_data += "&client_secret="; - post_data += secret; - } - if (!redirect_uri.empty()) { - post_data += "&redirect_uri="; - post_data += redirect_uri; - } - - if (!auth_code.empty()) { - post_data += "&grant_type=authorization_code&code="; - post_data += auth_code; - } else { - post_data += "&grant_type=refresh_token&refresh_token="; - post_data += refresh_token; - } - - bool success = false; - - auto func = [&]() { - success = GetRemoteFile(url, output, error, nullptr, "application/x-www-form-urlencoded", "", - post_data.c_str(), std::vector(), nullptr, 5); - }; - - ExecThreadedWithoutBlocking(func, QTStr("Auth.Authing.Title"), QTStr("Auth.Authing.Text").arg(service())); - if (!success || output.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - Json json = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - /* -------------------------- */ - /* error handling */ - - error = json["error"].string_value(); - if (!retry && error == "invalid_grant") { - if (RetryLogin()) { - return true; - } - } - if (!error.empty()) - throw ErrorInfo(error, json["error_description"].string_value()); - - /* -------------------------- */ - /* success! */ - - expire_time = (uint64_t)time(nullptr) + json["expires_in"].int_value(); - token = json["access_token"].string_value(); - if (token.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - if (!auth_code.empty()) { - refresh_token = json["refresh_token"].string_value(); - if (refresh_token.empty()) - throw ErrorInfo("Failed to get refresh token from " - "remote", - error); - - currentScopeVer = scope_ver; - } - - return true; - -} catch (ErrorInfo &info) { - if (!retry) { - QString title = QTStr("Auth.AuthFailure.Title"); - QString text = QTStr("Auth.AuthFailure.Text").arg(service(), info.message.c_str(), info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), info.error.c_str()); - return false; -} - -void OAuthStreamKey::OnStreamConfig() -{ - if (key_.empty()) - return; - - OBSBasic *main = OBSBasic::Get(); - obs_service_t *service = main->GetService(); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - - bool bwtest = obs_data_get_bool(settings, "bwtest"); - - if (bwtest && strcmp(this->service(), "Twitch") == 0) - obs_data_set_string(settings, "key", (key_ + "?bandwidthtest=true").c_str()); - else - obs_data_set_string(settings, "key", key_.c_str()); - - obs_service_update(service, settings); -} diff --git a/frontend/dialogs/OAuthLogin.hpp b/frontend/dialogs/OAuthLogin.hpp index b6602e815..9e6e94b49 100644 --- a/frontend/dialogs/OAuthLogin.hpp +++ b/frontend/dialogs/OAuthLogin.hpp @@ -1,10 +1,6 @@ #pragma once #include -#include -#include - -#include "auth-base.hpp" class QCefWidget; @@ -30,53 +26,3 @@ public: public slots: void urlChanged(const QString &url); }; - -class OAuth : public Auth { - Q_OBJECT - -public: - inline OAuth(const Def &d) : Auth(d) {} - - typedef std::function(QWidget *, const std::string &service_name)> login_cb; - typedef std::function delete_cookies_cb; - - static std::shared_ptr Login(QWidget *parent, const std::string &service); - static void DeleteCookies(const std::string &service); - - static void RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies); - -protected: - std::string refresh_token; - std::string token; - bool implicit = false; - uint64_t expire_time = 0; - int currentScopeVer = 0; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - virtual bool RetryLogin() = 0; - bool TokenExpired(); - bool GetToken(const char *url, const std::string &client_id, int scope_ver, - const std::string &auth_code = std::string(), bool retry = false); - bool GetToken(const char *url, const std::string &client_id, const std::string &secret, - const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry); - -private: - bool GetTokenInternal(const char *url, const std::string &client_id, const std::string &secret, - const std::string &redirect_uri, int scope_ver, const std::string &auth_code, bool retry); -}; - -class OAuthStreamKey : public OAuth { - Q_OBJECT - -protected: - std::string key_; - -public: - inline OAuthStreamKey(const Def &d) : OAuth(d) {} - - inline const std::string &key() const { return key_; } - - virtual void OnStreamConfig() override; -}; diff --git a/frontend/docks/YouTubeChatDock.cpp b/frontend/docks/YouTubeChatDock.cpp index 714003145..eae3a2b7c 100644 --- a/frontend/docks/YouTubeChatDock.cpp +++ b/frontend/docks/YouTubeChatDock.cpp @@ -1,323 +1,15 @@ -#include "moc_auth-youtube.cpp" +#include "YouTubeChatDock.hpp" + +#include +#include +#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#ifdef WIN32 -#include -#include +#include +#include -#pragma comment(lib, "shell32") -#endif - -#include "auth-listener.hpp" -#include "obs-app.hpp" -#include "ui-config.h" -#include "youtube-api-wrappers.hpp" -#include "window-basic-main.hpp" -#include "obf.h" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#endif - -using namespace json11; - -/* ------------------------------------------------------------------------- */ -#define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" -#define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" -#define YOUTUBE_SCOPE_VERSION 1 -#define YOUTUBE_API_STATE_LENGTH 32 -#define SECTION_NAME "YouTube" - -#define YOUTUBE_CHAT_PLACEHOLDER_URL "https://obsproject.com/placeholders/youtube-chat" -#define YOUTUBE_CHAT_POPOUT_URL "https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v=%1" - -#define YOUTUBE_CHAT_DOCK_NAME "ytChat" - -static const char allowedChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -static const int allowedCount = static_cast(sizeof(allowedChars) - 1); -/* ------------------------------------------------------------------------- */ - -static inline void OpenBrowser(const QString auth_uri) -{ - QUrl url(auth_uri, QUrl::StrictMode); - QDesktopServices::openUrl(url); -} - -static void DeleteCookies() -{ - if (panel_cookies) { - panel_cookies->DeleteCookies("youtube.com", ""); - panel_cookies->DeleteCookies("google.com", ""); - } -} - -void RegisterYoutubeAuth() -{ - for (auto &service : youtubeServices) { - OAuth::RegisterOAuth( - service, [service]() { return std::make_shared(service); }, - YoutubeAuth::Login, DeleteCookies); - } -} - -YoutubeAuth::YoutubeAuth(const Def &d) : OAuthStreamKey(d), section(SECTION_NAME) {} - -YoutubeAuth::~YoutubeAuth() -{ - if (!uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(YOUTUBE_CHAT_DOCK_NAME); - chat = nullptr; -#endif -} - -bool YoutubeAuth::RetryLogin() -{ - return true; -} - -void YoutubeAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "DockState", main->saveState().toBase64().constData()); - - const char *section_name = section.c_str(); - config_set_string(main->Config(), section_name, "RefreshToken", refresh_token.c_str()); - config_set_string(main->Config(), section_name, "Token", token.c_str()); - config_set_uint(main->Config(), section_name, "ExpireTime", expire_time); - config_set_int(main->Config(), section_name, "ScopeVer", currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool YoutubeAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - - const char *section_name = section.c_str(); - refresh_token = get_config_str(main, section_name, "RefreshToken"); - token = get_config_str(main, section_name, "Token"); - expire_time = config_get_uint(main->Config(), section_name, "ExpireTime"); - currentScopeVer = (int)config_get_int(main->Config(), section_name, "ScopeVer"); - firstLoad = false; - return implicit ? !token.empty() : !refresh_token.empty(); -} - -void YoutubeAuth::LoadUI() -{ - if (uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - if (!cef) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - chat = new YoutubeChatDock(QTStr("Auth.Chat")); - chat->setObjectName(YOUTUBE_CHAT_DOCK_NAME); - chat->resize(300, 600); - chat->setMinimumSize(200, 300); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, YOUTUBE_CHAT_PLACEHOLDER_URL, panel_cookies); - - chat->SetWidget(browser); - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - chat->setFloating(true); - chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); - - if (firstLoad) { - chat->setVisible(true); - } -#endif - - main->NewYouTubeAppDock(); - - if (!firstLoad) { - const char *dockStateStr = config_get_string(main->Config(), service(), "DockState"); - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); - } - - uiLoaded = true; -} - -void YoutubeAuth::SetChatId(const QString &chat_id) -{ -#ifdef BROWSER_AVAILABLE - QString chat_url = QString(YOUTUBE_CHAT_POPOUT_URL).arg(chat_id); - - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(chat_url.toStdString()); - } -#else - UNUSED_PARAMETER(chat_id); -#endif -} - -void YoutubeAuth::ResetChat() -{ -#ifdef BROWSER_AVAILABLE - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(YOUTUBE_CHAT_PLACEHOLDER_URL); - } -#endif -} - -void YoutubeAuth::ReloadChat() -{ -#ifdef BROWSER_AVAILABLE - if (chat && chat->cefWidget) { - chat->cefWidget->reloadPage(); - } -#endif -} - -QString YoutubeAuth::GenerateState() -{ - char state[YOUTUBE_API_STATE_LENGTH + 1]; - QRandomGenerator *rng = QRandomGenerator::system(); - int i; - - for (i = 0; i < YOUTUBE_API_STATE_LENGTH; i++) - state[i] = allowedChars[rng->bounded(0, allowedCount)]; - state[i] = 0; - - return state; -} - -// Static. -std::shared_ptr YoutubeAuth::Login(QWidget *owner, const std::string &service) -{ - QString auth_code; - AuthListener server; - - auto it = std::find_if(youtubeServices.begin(), youtubeServices.end(), - [service](auto &item) { return service == item.service; }); - if (it == youtubeServices.end()) { - return nullptr; - } - const auto auth = std::make_shared(*it); - - QString redirect_uri = QString("http://127.0.0.1:%1").arg(server.GetPort()); - - QMessageBox dlg(owner); - dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); - dlg.setWindowTitle(QTStr("YouTube.Auth.WaitingAuth.Title")); - - std::string clientid = YOUTUBE_CLIENTID; - std::string secret = YOUTUBE_SECRET; - deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); - deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); - - QString state; - state = auth->GenerateState(); - server.SetState(state); - - QString url_template; - url_template += "%1"; - url_template += "?response_type=code"; - url_template += "&client_id=%2"; - url_template += "&redirect_uri=%3"; - url_template += "&state=%4"; - url_template += "&scope=https://www.googleapis.com/auth/youtube"; - QString url = url_template.arg(YOUTUBE_AUTH_URL, clientid.c_str(), redirect_uri, state); - - QString text = QTStr("YouTube.Auth.WaitingAuth.Text"); - text = text.arg(QString("Google OAuth Service").arg(url)); - - dlg.setText(text); - dlg.setTextFormat(Qt::RichText); - dlg.setStandardButtons(QMessageBox::StandardButton::Cancel); -#if defined(__APPLE__) && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - /* We can't show clickable links with the native NSAlert, so let's - * force the old non-native dialog instead. */ - dlg.setOption(QMessageBox::Option::DontUseNativeDialog); -#endif - - connect(&dlg, &QMessageBox::buttonClicked, &dlg, [&](QAbstractButton *) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Action Cancelled."); -#endif - // TODO: Stop server. - dlg.reject(); - }); - - // Async Login. - connect(&server, &AuthListener::ok, &dlg, [&dlg, &auth_code](QString code) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Got youtube redirected answer: %s", QT_TO_UTF8(code)); -#endif - auth_code = code; - dlg.accept(); - }); - connect(&server, &AuthListener::fail, &dlg, [&dlg]() { -#ifdef _DEBUG - blog(LOG_DEBUG, "No access granted"); -#endif - dlg.reject(); - }); - - auto open_external_browser = [url]() { - OpenBrowser(url); - }; - QScopedPointer thread(CreateQThread(open_external_browser)); - thread->start(); - -#if defined(__APPLE__) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && QT_VERSION < QT_VERSION_CHECK(6, 6, 0) - const bool nativeDialogs = qApp->testAttribute(Qt::AA_DontUseNativeDialogs); - App()->setAttribute(Qt::AA_DontUseNativeDialogs, true); - dlg.exec(); - App()->setAttribute(Qt::AA_DontUseNativeDialogs, nativeDialogs); -#else - dlg.exec(); -#endif - - if (dlg.result() == QMessageBox::Cancel || dlg.result() == QDialog::Rejected) - return nullptr; - - if (!auth->GetToken(YOUTUBE_TOKEN_URL, clientid, secret, QT_TO_UTF8(redirect_uri), YOUTUBE_SCOPE_VERSION, - QT_TO_UTF8(auth_code), true)) { - return nullptr; - } - - config_t *config = OBSBasic::Get()->Config(); - config_remove_value(config, "YouTube", "ChannelName"); - - ChannelDescription cd; - if (auth->GetChannelDescription(cd)) - config_set_string(config, "YouTube", "ChannelName", QT_TO_UTF8(cd.title)); - - config_save_safe(config, "tmp", nullptr); - return auth; -} +#include "moc_YouTubeChatDock.cpp" #ifdef BROWSER_AVAILABLE void YoutubeChatDock::YoutubeCookieCheck() diff --git a/frontend/docks/YouTubeChatDock.hpp b/frontend/docks/YouTubeChatDock.hpp index cd560db7d..034be0e67 100644 --- a/frontend/docks/YouTubeChatDock.hpp +++ b/frontend/docks/YouTubeChatDock.hpp @@ -1,15 +1,8 @@ #pragma once -#include -#include -#include -#include - -#include "auth-oauth.hpp" - #ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#include +#include "BrowserDock.hpp" + class YoutubeChatDock : public BrowserDock { Q_OBJECT @@ -28,35 +21,3 @@ private slots: void YoutubeCookieCheck(); }; #endif - -inline const std::vector youtubeServices = {{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; - -class YoutubeAuth : public OAuthStreamKey { - Q_OBJECT - - bool uiLoaded = false; - std::string section; - -#ifdef BROWSER_AVAILABLE - YoutubeChatDock *chat = nullptr; -#endif - - virtual bool RetryLogin() override; - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - virtual void LoadUI() override; - - QString GenerateState(); - -public: - YoutubeAuth(const Def &d); - ~YoutubeAuth(); - - void SetChatId(const QString &chat_id); - void ResetChat(); - void ReloadChat(); - - static std::shared_ptr Login(QWidget *parent, const std::string &service); -}; diff --git a/frontend/oauth/OAuth.cpp b/frontend/oauth/OAuth.cpp index 04458c9c5..a7d6f60d6 100644 --- a/frontend/oauth/OAuth.cpp +++ b/frontend/oauth/OAuth.cpp @@ -1,124 +1,17 @@ -#include "moc_auth-oauth.cpp" +#include "OAuth.hpp" -#include -#include -#include +#include +#include #include -#include - -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include +#include #include -#include "ui-config.h" +#include "moc_OAuth.cpp" using namespace json11; -#ifdef BROWSER_AVAILABLE -#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; -#endif - -/* ------------------------------------------------------------------------- */ - -OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) -{ -#ifdef BROWSER_AVAILABLE - if (!cef) { - return; - } - - setWindowTitle("Auth"); - setMinimumSize(400, 400); - resize(700, 700); - - Qt::WindowFlags flags = windowFlags(); - Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; - setWindowFlags(flags & (~helpFlag)); - - OBSBasic::InitBrowserPanelSafeBlock(); - - cefWidget = cef->create_widget(nullptr, url, panel_cookies); - if (!cefWidget) { - fail = true; - return; - } - - connect(cefWidget, &QCefWidget::titleChanged, this, &OAuthLogin::setWindowTitle); - connect(cefWidget, &QCefWidget::urlChanged, this, &OAuthLogin::urlChanged); - - QPushButton *close = new QPushButton(QTStr("Cancel")); - connect(close, &QAbstractButton::clicked, this, &QDialog::reject); - - QHBoxLayout *bottomLayout = new QHBoxLayout(); - bottomLayout->addStretch(); - bottomLayout->addWidget(close); - bottomLayout->addStretch(); - - QVBoxLayout *topLayout = new QVBoxLayout(this); - topLayout->addWidget(cefWidget); - topLayout->addLayout(bottomLayout); -#else - UNUSED_PARAMETER(url); -#endif -} - -OAuthLogin::~OAuthLogin() {} - -int OAuthLogin::exec() -{ -#ifdef BROWSER_AVAILABLE - if (cefWidget) { - return QDialog::exec(); - } -#endif - return QDialog::Rejected; -} - -void OAuthLogin::reject() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::reject(); -} - -void OAuthLogin::accept() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::accept(); -} - -void OAuthLogin::urlChanged(const QString &url) -{ - std::string uri = get_token ? "access_token=" : "code="; - int code_idx = url.indexOf(uri.c_str()); - if (code_idx == -1) - return; - - if (!url.startsWith(OAUTH_BASE_URL)) - return; - - code_idx += (int)uri.size(); - - int next_idx = url.indexOf("&", code_idx); - if (next_idx != -1) - code = url.mid(code_idx, next_idx - code_idx); - else - code = url.right(url.size() - code_idx); - - accept(); -} - -/* ------------------------------------------------------------------------- */ - struct OAuthInfo { Auth::Def def; OAuth::login_cb login; @@ -126,7 +19,6 @@ struct OAuthInfo { }; static std::vector loginCBs; - void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies) { OAuthInfo info = {d, login, delete_cookies}; diff --git a/frontend/oauth/OAuth.hpp b/frontend/oauth/OAuth.hpp index b6602e815..46bd720ca 100644 --- a/frontend/oauth/OAuth.hpp +++ b/frontend/oauth/OAuth.hpp @@ -1,35 +1,6 @@ #pragma once -#include -#include -#include - -#include "auth-base.hpp" - -class QCefWidget; - -class OAuthLogin : public QDialog { - Q_OBJECT - - QCefWidget *cefWidget = nullptr; - QString code; - bool get_token = false; - bool fail = false; - -public: - OAuthLogin(QWidget *parent, const std::string &url, bool token); - ~OAuthLogin(); - - inline QString GetCode() const { return code; } - inline bool LoadFail() const { return fail; } - - virtual int exec() override; - virtual void reject() override; - virtual void accept() override; - -public slots: - void urlChanged(const QString &url); -}; +#include "Auth.hpp" class OAuth : public Auth { Q_OBJECT diff --git a/frontend/oauth/YoutubeAuth.cpp b/frontend/oauth/YoutubeAuth.cpp index 714003145..9d626bb7e 100644 --- a/frontend/oauth/YoutubeAuth.cpp +++ b/frontend/oauth/YoutubeAuth.cpp @@ -1,36 +1,21 @@ -#include "moc_auth-youtube.cpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include - -#pragma comment(lib, "shell32") -#endif - -#include "auth-listener.hpp" -#include "obs-app.hpp" -#include "ui-config.h" -#include "youtube-api-wrappers.hpp" -#include "window-basic-main.hpp" -#include "obf.h" +#include "YoutubeAuth.hpp" #ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" +#include #endif +#include +#include +#include +#include -using namespace json11; +#include +#include + +#include +#include + +#include "moc_YoutubeAuth.cpp" -/* ------------------------------------------------------------------------- */ #define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" #define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" #define YOUTUBE_SCOPE_VERSION 1 @@ -318,26 +303,3 @@ std::shared_ptr YoutubeAuth::Login(QWidget *owner, const std::string &serv config_save_safe(config, "tmp", nullptr); return auth; } - -#ifdef BROWSER_AVAILABLE -void YoutubeChatDock::YoutubeCookieCheck() -{ - QPointer this_ = this; - auto cb = [this_](bool currentlyLoggedIn) { - bool previouslyLoggedIn = this_->isLoggedIn; - this_->isLoggedIn = currentlyLoggedIn; - bool loginStateChanged = (currentlyLoggedIn && !previouslyLoggedIn) || - (!currentlyLoggedIn && previouslyLoggedIn); - if (loginStateChanged) { - OBSBasic *main = OBSBasic::Get(); - if (main->GetYouTubeAppDock() != nullptr) { - QMetaObject::invokeMethod(main->GetYouTubeAppDock(), "SettingsUpdated", - Qt::QueuedConnection, Q_ARG(bool, !currentlyLoggedIn)); - } - } - }; - if (panel_cookies) { - panel_cookies->CheckForCookie("https://www.youtube.com", "SID", cb); - } -} -#endif diff --git a/frontend/oauth/YoutubeAuth.hpp b/frontend/oauth/YoutubeAuth.hpp index cd560db7d..cecc5f2ac 100644 --- a/frontend/oauth/YoutubeAuth.hpp +++ b/frontend/oauth/YoutubeAuth.hpp @@ -1,38 +1,15 @@ #pragma once -#include -#include -#include -#include - -#include "auth-oauth.hpp" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#include -class YoutubeChatDock : public BrowserDock { - Q_OBJECT - -private: - bool isLoggedIn; - -public: - YoutubeChatDock(const QString &title) : BrowserDock(title) {} - - inline void SetWidget(QCefWidget *widget_) - { - BrowserDock::SetWidget(widget_); - QWidget::connect(cefWidget.get(), &QCefWidget::urlChanged, this, &YoutubeChatDock::YoutubeCookieCheck); - } -private slots: - void YoutubeCookieCheck(); -}; -#endif +#include "OAuth.hpp" inline const std::vector youtubeServices = {{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; +#ifdef BROWSER_AVAILABLE +class YoutubeChatDock; +#endif + class YoutubeAuth : public OAuthStreamKey { Q_OBJECT From 7f6bee603b1114d96f0309c43a7ed6e5e3bf9a44 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 20:52:29 +0100 Subject: [PATCH 13/37] frontend: Add renamed Qt Settings UI implementation --- .../settings/OBSBasicSettings_A11y.cpp | 9 +++--- .../settings/OBSBasicSettings_Appearance.cpp | 20 +++--------- .../settings/OBSBasicSettings_Stream.cpp | 31 +++++++------------ 3 files changed, 20 insertions(+), 40 deletions(-) rename UI/window-basic-settings-a11y.cpp => frontend/settings/OBSBasicSettings_A11y.cpp (98%) rename UI/window-basic-settings-appearance.cpp => frontend/settings/OBSBasicSettings_Appearance.cpp (88%) rename UI/window-basic-settings-stream.cpp => frontend/settings/OBSBasicSettings_Stream.cpp (99%) diff --git a/UI/window-basic-settings-a11y.cpp b/frontend/settings/OBSBasicSettings_A11y.cpp similarity index 98% rename from UI/window-basic-settings-a11y.cpp rename to frontend/settings/OBSBasicSettings_A11y.cpp index d50cbfded..15bf42435 100644 --- a/UI/window-basic-settings-a11y.cpp +++ b/frontend/settings/OBSBasicSettings_A11y.cpp @@ -1,8 +1,7 @@ -#include "window-basic-settings.hpp" -#include "window-basic-main.hpp" -#include "obs-frontend-api.h" -#include "obs-app.hpp" -#include +#include "OBSBasicSettings.hpp" + +#include + #include enum ColorPreset { diff --git a/UI/window-basic-settings-appearance.cpp b/frontend/settings/OBSBasicSettings_Appearance.cpp similarity index 88% rename from UI/window-basic-settings-appearance.cpp rename to frontend/settings/OBSBasicSettings_Appearance.cpp index cdfbc92f5..80e3fc914 100644 --- a/UI/window-basic-settings-appearance.cpp +++ b/frontend/settings/OBSBasicSettings_Appearance.cpp @@ -1,21 +1,9 @@ -#include "window-basic-settings.hpp" -#include "window-basic-main.hpp" -#include "obs-frontend-api.h" -#include "qt-wrappers.hpp" -#include "platform.hpp" -#include "obs-app.hpp" +#include "OBSBasicSettings.hpp" -#include -#include -#include -#include -#include -#include -#include +#include +#include -#include "util/profiler.hpp" - -using namespace std; +#include void OBSBasicSettings::InitAppearancePage() { diff --git a/UI/window-basic-settings-stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp similarity index 99% rename from UI/window-basic-settings-stream.cpp rename to frontend/settings/OBSBasicSettings_Stream.cpp index 885c49f8a..eff5d7de3 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/frontend/settings/OBSBasicSettings_Stream.cpp @@ -1,25 +1,17 @@ -#include -#include -#include -#include - -#include "window-basic-settings.hpp" -#include "obs-frontend-api.h" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "url-push-button.hpp" - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" - -#include "ui-config.h" +#include "OBSBasicSettings.hpp" #ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" +#include #endif +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include + +#include + +#include static const QUuid &CustomServerUUID() { @@ -32,6 +24,7 @@ struct QCefCookieManager; extern QCef *cef; extern QCefCookieManager *panel_cookies; +extern bool cef_js_avail; enum class ListOpt : int { ShowAll = 1, From 9876882e0bbee6274fe655054ea08fb038bda306 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 18:54:03 +0100 Subject: [PATCH 14/37] frontend: Prepare Qt Settings UI files for splits --- .../components/SilentUpdateCheckBox.hpp | 0 frontend/components/SilentUpdateSpinBox.hpp | 489 ++ .../settings/OBSBasicSettings.cpp | 0 frontend/settings/OBSBasicSettings.hpp | 491 ++ .../settings/OBSHotkeyEdit.cpp | 0 .../settings/OBSHotkeyEdit.hpp | 0 frontend/settings/OBSHotkeyLabel.cpp | 470 ++ frontend/settings/OBSHotkeyLabel.hpp | 190 + frontend/settings/OBSHotkeyWidget.cpp | 470 ++ frontend/settings/OBSHotkeyWidget.hpp | 190 + frontend/utility/SettingsEventFilter.hpp | 5825 +++++++++++++++++ 11 files changed, 8125 insertions(+) rename UI/window-basic-settings.hpp => frontend/components/SilentUpdateCheckBox.hpp (100%) create mode 100644 frontend/components/SilentUpdateSpinBox.hpp rename UI/window-basic-settings.cpp => frontend/settings/OBSBasicSettings.cpp (100%) create mode 100644 frontend/settings/OBSBasicSettings.hpp rename UI/hotkey-edit.cpp => frontend/settings/OBSHotkeyEdit.cpp (100%) rename UI/hotkey-edit.hpp => frontend/settings/OBSHotkeyEdit.hpp (100%) create mode 100644 frontend/settings/OBSHotkeyLabel.cpp create mode 100644 frontend/settings/OBSHotkeyLabel.hpp create mode 100644 frontend/settings/OBSHotkeyWidget.cpp create mode 100644 frontend/settings/OBSHotkeyWidget.hpp create mode 100644 frontend/utility/SettingsEventFilter.hpp diff --git a/UI/window-basic-settings.hpp b/frontend/components/SilentUpdateCheckBox.hpp similarity index 100% rename from UI/window-basic-settings.hpp rename to frontend/components/SilentUpdateCheckBox.hpp diff --git a/frontend/components/SilentUpdateSpinBox.hpp b/frontend/components/SilentUpdateSpinBox.hpp new file mode 100644 index 000000000..05d46fd41 --- /dev/null +++ b/frontend/components/SilentUpdateSpinBox.hpp @@ -0,0 +1,489 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "auth-base.hpp" +#include "ffmpeg-utils.hpp" +#include "obs-app-theming.hpp" + +class OBSBasic; +class QAbstractButton; +class QRadioButton; +class QComboBox; +class QCheckBox; +class QLabel; +class QButtonGroup; +class OBSPropertiesView; +class OBSHotkeyWidget; + +#include "ui_OBSBasicSettings.h" + +#define VOLUME_METER_DECAY_FAST 23.53 +#define VOLUME_METER_DECAY_MEDIUM 11.76 +#define VOLUME_METER_DECAY_SLOW 8.57 + +class SilentUpdateCheckBox : public QCheckBox { + Q_OBJECT + +public slots: + void setCheckedSilently(bool checked) + { + bool blocked = blockSignals(true); + setChecked(checked); + blockSignals(blocked); + } +}; + +class SilentUpdateSpinBox : public QSpinBox { + Q_OBJECT + +public slots: + void setValueSilently(int val) + { + bool blocked = blockSignals(true); + setValue(val); + blockSignals(blocked); + } +}; + +std::string DeserializeConfigText(const char *value); + +class OBSBasicSettings : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true) + Q_PROPERTY(QIcon appearanceIcon READ GetAppearanceIcon WRITE SetAppearanceIcon DESIGNABLE true) + Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true) + Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true) + Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true) + Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true) + Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true) + Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true) + + enum Pages { GENERAL, APPEARANCE, STREAM, OUTPUT, AUDIO, VIDEO, HOTKEYS, ACCESSIBILITY, ADVANCED, NUM_PAGES }; + +private: + OBSBasic *main; + + std::unique_ptr ui; + + std::shared_ptr auth; + + bool generalChanged = false; + bool stream1Changed = false; + bool outputsChanged = false; + bool audioChanged = false; + bool videoChanged = false; + bool hotkeysChanged = false; + bool a11yChanged = false; + bool appearanceChanged = false; + bool advancedChanged = false; + int pageIndex = 0; + bool loading = true; + bool forceAuthReload = false; + bool forceUpdateCheck = false; + int sampleRateIndex = 0; + int channelIndex = 0; + bool llBufferingEnabled = false; + bool hotkeysLoaded = false; + + int lastSimpleRecQualityIdx = 0; + int lastServiceIdx = -1; + int lastIgnoreRecommended = -1; + int lastChannelSetupIdx = 0; + + static constexpr uint32_t ENCODER_HIDE_FLAGS = (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL); + + OBSTheme *savedTheme = nullptr; + + std::vector formats; + + OBSPropertiesView *streamProperties = nullptr; + OBSPropertiesView *streamEncoderProps = nullptr; + OBSPropertiesView *recordEncoderProps = nullptr; + + QPointer advOutRecWarning; + QPointer simpleOutRecWarning; + + QString curPreset; + QString curQSVPreset; + QString curNVENCPreset; + QString curAMDPreset; + QString curAMDAV1Preset; + + QString curAdvStreamEncoder; + QString curAdvRecordEncoder; + + using AudioSource_t = std::tuple, QPointer, QPointer, + QPointer>; + std::vector audioSources; + std::vector audioSourceSignals; + OBSSignal sourceCreated; + OBSSignal channelChanged; + + std::vector>> hotkeys; + OBSSignal hotkeyRegistered; + OBSSignal hotkeyUnregistered; + + uint32_t outputCX = 0; + uint32_t outputCY = 0; + + QPointer simpleVodTrack; + + QPointer vodTrackCheckbox; + QPointer vodTrackContainer; + QPointer vodTrack[MAX_AUDIO_MIXES]; + + QIcon hotkeyConflictIcon; + + void SaveCombo(QComboBox *widget, const char *section, const char *value); + void SaveComboData(QComboBox *widget, const char *section, const char *value); + void SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert = false); + void SaveGroupBox(QGroupBox *widget, const char *section, const char *value); + void SaveEdit(QLineEdit *widget, const char *section, const char *value); + void SaveSpinBox(QSpinBox *widget, const char *section, const char *value); + void SaveText(QPlainTextEdit *widget, const char *section, const char *value); + void SaveFormat(QComboBox *combo); + void SaveEncoder(QComboBox *combo, const char *section, const char *value); + + bool ResFPSValid(obs_service_resolution *res_list, size_t res_count, int max_fps); + void ClosestResFPS(obs_service_resolution *res_list, size_t res_count, int max_fps, int &new_cx, int &new_cy, + int &new_fps); + + inline bool Changed() const + { + return generalChanged || appearanceChanged || outputsChanged || stream1Changed || audioChanged || + videoChanged || advancedChanged || hotkeysChanged || a11yChanged; + } + + inline void EnableApplyButton(bool en) { ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(en); } + + inline void ClearChanged() + { + generalChanged = false; + stream1Changed = false; + outputsChanged = false; + audioChanged = false; + videoChanged = false; + hotkeysChanged = false; + a11yChanged = false; + advancedChanged = false; + appearanceChanged = false; + EnableApplyButton(false); + } + + template + void HookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), + void (OBSBasicSettings::*slot)(SlotArgs...)) + { + QObject::connect(widget, signal, this, slot); + widget->setProperty("changed", QVariant(false)); + } + + bool QueryChanges(); + bool QueryAllowedToClose(); + + void ResetEncoders(bool streamOnly = false); + void LoadColorRanges(); + void LoadColorSpaces(); + void LoadColorFormats(); + void LoadFormats(); + void ReloadCodecs(const FFmpegFormat &format); + + void UpdateColorFormatSpaceWarning(); + + void LoadGeneralSettings(); + void LoadStream1Settings(); + void LoadOutputSettings(); + void LoadAudioSettings(); + void LoadVideoSettings(); + void LoadHotkeySettings(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); + void LoadA11ySettings(bool presetChange = false); + void LoadAppearanceSettings(bool reload = false); + void LoadAdvancedSettings(); + void LoadSettings(bool changedOnly); + + OBSPropertiesView *CreateEncoderPropertyView(const char *encoder, const char *path, bool changed = false); + + /* general */ + void LoadLanguageList(); + void LoadThemeList(bool firstLoad); + void LoadBranchesList(); + + /* stream */ + void InitStreamPage(); + bool IsCustomService() const; + inline bool IsWHIP() const; + void LoadServices(bool showAll); + void OnOAuthStreamKeyConnected(); + void OnAuthConnected(); + QString lastService; + QString protocol; + QString lastCustomServer; + int prevLangIndex; + bool prevBrowserAccel; + + void ServiceChanged(bool resetFields = false); + QString FindProtocol(); + void UpdateServerList(); + void UpdateKeyLink(); + void UpdateVodTrackSetting(); + void UpdateServiceRecommendations(); + void UpdateMoreInfoLink(); + void UpdateAdvNetworkGroup(); + + /* Appearance */ + void InitAppearancePage(); + + bool IsCustomServer(); + +private slots: + void UpdateMultitrackVideo(); + void RecreateOutputResolutionWidget(); + bool UpdateResFPSLimits(); + void DisplayEnforceWarning(bool checked); + void on_show_clicked(); + void on_authPwShow_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_useAuth_toggled(); + void on_server_currentIndexChanged(int index); + + void on_hotkeyFilterReset_clicked(); + void on_hotkeyFilterSearch_textChanged(const QString text); + void on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo); + +private: + /* output */ + void LoadSimpleOutputSettings(); + void LoadAdvOutputStreamingSettings(); + void LoadAdvOutputStreamingEncoderProperties(); + void LoadAdvOutputRecordingSettings(); + void LoadAdvOutputRecordingEncoderProperties(); + void LoadAdvOutputFFmpegSettings(); + void LoadAdvOutputAudioSettings(); + void SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncode = false); + + /* audio */ + void LoadListValues(QComboBox *widget, obs_property_t *prop, int index); + void LoadAudioDevices(); + void LoadAudioSources(); + + /* video */ + void LoadRendererList(); + void ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals = false); + void LoadDownscaleFilters(); + void LoadResolutionLists(); + void LoadFPSData(); + + /* a11y */ + void UpdateA11yColors(); + void SetDefaultColors(); + void ResetDefaultColors(); + QColor GetColor(uint32_t colorVal, QString label); + uint32_t preset = 0; + uint32_t selectRed = 0x0000FF; + uint32_t selectGreen = 0x00FF00; + uint32_t selectBlue = 0xFF7F00; + uint32_t mixerGreen = 0x267f26; + uint32_t mixerYellow = 0x267f7f; + uint32_t mixerRed = 0x26267f; + uint32_t mixerGreenActive = 0x4cff4c; + uint32_t mixerYellowActive = 0x4cffff; + uint32_t mixerRedActive = 0x4c4cff; + + void SaveGeneralSettings(); + void SaveStream1Settings(); + void SaveOutputSettings(); + void SaveAudioSettings(); + void SaveVideoSettings(); + void SaveHotkeySettings(); + void SaveA11ySettings(); + void SaveAppearanceSettings(); + void SaveAdvancedSettings(); + void SaveSettings(); + + void SearchHotkeys(const QString &text, obs_key_combination_t filterCombo); + + void UpdateSimpleOutStreamDelayEstimate(); + void UpdateAdvOutStreamDelayEstimate(); + + void FillSimpleRecordingValues(); + void FillAudioMonitoringDevices(); + + void RecalcOutputResPixels(const char *resText); + + bool AskIfCanCloseSettings(); + + void UpdateYouTubeAppDockSettings(); + + QIcon generalIcon; + QIcon appearanceIcon; + QIcon streamIcon; + QIcon outputIcon; + QIcon audioIcon; + QIcon videoIcon; + QIcon hotkeysIcon; + QIcon accessibilityIcon; + QIcon advancedIcon; + + QIcon GetGeneralIcon() const; + QIcon GetAppearanceIcon() const; + QIcon GetStreamIcon() const; + QIcon GetOutputIcon() const; + QIcon GetAudioIcon() const; + QIcon GetVideoIcon() const; + QIcon GetHotkeysIcon() const; + QIcon GetAccessibilityIcon() const; + QIcon GetAdvancedIcon() const; + + int CurrentFLVTrack(); + int SimpleOutGetSelectedAudioTracks(); + int AdvOutGetSelectedAudioTracks(); + int AdvOutGetStreamingSelectedAudioTracks(); + + OBSService GetStream1Service(); + + bool ServiceAndVCodecCompatible(); + bool ServiceAndACodecCompatible(); + bool ServiceSupportsCodecCheck(); + + inline bool AllowsMultiTrack(const char *protocol); + void SwapMultiTrack(const char *protocol); + +private slots: + void on_theme_activated(int idx); + void on_themeVariant_activated(int idx); + + void on_listWidget_itemSelectionChanged(); + void on_buttonBox_clicked(QAbstractButton *button); + + void on_service_currentIndexChanged(int idx); + void on_customServer_textChanged(const QString &text); + void on_simpleOutputBrowse_clicked(); + void on_advOutRecPathBrowse_clicked(); + void on_advOutFFPathBrowse_clicked(); + void on_advOutEncoder_currentIndexChanged(); + void on_advOutRecEncoder_currentIndexChanged(int idx); + void on_advOutFFIgnoreCompat_stateChanged(int state); + void on_advOutFFFormat_currentIndexChanged(int idx); + void on_advOutFFAEncoder_currentIndexChanged(int idx); + void on_advOutFFVEncoder_currentIndexChanged(int idx); + void on_advOutFFType_currentIndexChanged(int idx); + + void on_colorFormat_currentIndexChanged(int idx); + void on_colorSpace_currentIndexChanged(int idx); + + void on_filenameFormatting_textEdited(const QString &text); + void on_outputResolution_editTextChanged(const QString &text); + void on_baseResolution_editTextChanged(const QString &text); + + void on_disableOSXVSync_clicked(); + + void on_choose1_clicked(); + void on_choose2_clicked(); + void on_choose3_clicked(); + void on_choose4_clicked(); + void on_choose5_clicked(); + void on_choose6_clicked(); + void on_choose7_clicked(); + void on_choose8_clicked(); + void on_choose9_clicked(); + void on_colorPreset_currentIndexChanged(int idx); + + void GeneralChanged(); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + void HideOBSWindowWarning(Qt::CheckState state); +#else + void HideOBSWindowWarning(int state); +#endif + void AudioChanged(); + void AudioChangedRestart(); + void ReloadAudioSources(); + void SurroundWarning(int idx); + void SpeakerLayoutChanged(int idx); + void LowLatencyBufferingChanged(bool checked); + void UpdateAudioWarnings(); + void OutputsChanged(); + void Stream1Changed(); + void VideoChanged(); + void VideoChangedResolution(); + void HotkeysChanged(); + bool ScanDuplicateHotkeys(QFormLayout *layout); + void ReloadHotkeys(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); + void A11yChanged(); + void AppearanceChanged(); + void AdvancedChanged(); + void AdvancedChangedRestart(); + + void UpdateStreamDelayEstimate(); + + void UpdateAutomaticReplayBufferCheckboxes(); + + void AdvOutSplitFileChanged(); + void AdvOutRecCheckWarnings(); + void AdvOutRecCheckCodecs(); + + void SimpleRecordingQualityChanged(); + void SimpleRecordingEncoderChanged(); + void SimpleRecordingQualityLosslessWarning(int idx); + + void SimpleReplayBufferChanged(); + void AdvReplayBufferChanged(); + + void SimpleStreamingEncoderChanged(); + + OBSService SpawnTempService(); + + void SetGeneralIcon(const QIcon &icon); + void SetAppearanceIcon(const QIcon &icon); + void SetStreamIcon(const QIcon &icon); + void SetOutputIcon(const QIcon &icon); + void SetAudioIcon(const QIcon &icon); + void SetVideoIcon(const QIcon &icon); + void SetHotkeysIcon(const QIcon &icon); + void SetAccessibilityIcon(const QIcon &icon); + void SetAdvancedIcon(const QIcon &icon); + + void UseStreamKeyAdvClicked(); + + void SimpleStreamAudioEncoderChanged(); + void AdvAudioEncodersChanged(); + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual void showEvent(QShowEvent *event) override; + void reject() override; + +public: + OBSBasicSettings(QWidget *parent); + ~OBSBasicSettings(); + + inline const QIcon &GetHotkeyConflictIcon() const { return hotkeyConflictIcon; } +}; diff --git a/UI/window-basic-settings.cpp b/frontend/settings/OBSBasicSettings.cpp similarity index 100% rename from UI/window-basic-settings.cpp rename to frontend/settings/OBSBasicSettings.cpp diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp new file mode 100644 index 000000000..47e8df795 --- /dev/null +++ b/frontend/settings/OBSBasicSettings.hpp @@ -0,0 +1,491 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "auth-base.hpp" +#include "ffmpeg-utils.hpp" +#include "obs-app-theming.hpp" + +class OBSBasic; +class QAbstractButton; +class QRadioButton; +class QComboBox; +class QCheckBox; +class QLabel; +class QButtonGroup; +class OBSPropertiesView; +class OBSHotkeyWidget; + +#include "ui_OBSBasicSettings.h" + +#define VOLUME_METER_DECAY_FAST 23.53 +#define VOLUME_METER_DECAY_MEDIUM 11.76 +#define VOLUME_METER_DECAY_SLOW 8.57 + +class SilentUpdateCheckBox : public QCheckBox { + Q_OBJECT + +public slots: + void setCheckedSilently(bool checked) + { + bool blocked = blockSignals(true); + setChecked(checked); + blockSignals(blocked); + } +}; + +class SilentUpdateSpinBox : public QSpinBox { + Q_OBJECT + +public slots: + void setValueSilently(int val) + { + bool blocked = blockSignals(true); + setValue(val); + blockSignals(blocked); + } +}; + +std::string DeserializeConfigText(const char *value); + +class OBSBasicSettings : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true) + Q_PROPERTY(QIcon appearanceIcon READ GetAppearanceIcon WRITE SetAppearanceIcon DESIGNABLE true) + Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true) + Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true) + Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true) + Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true) + Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true) + Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true) + + enum Pages { GENERAL, APPEARANCE, STREAM, OUTPUT, AUDIO, VIDEO, HOTKEYS, ACCESSIBILITY, ADVANCED, NUM_PAGES }; + +private: + OBSBasic *main; + + std::unique_ptr ui; + + std::shared_ptr auth; + + bool generalChanged = false; + bool stream1Changed = false; + bool outputsChanged = false; + bool audioChanged = false; + bool videoChanged = false; + bool hotkeysChanged = false; + bool a11yChanged = false; + bool appearanceChanged = false; + bool advancedChanged = false; + int pageIndex = 0; + bool loading = true; + bool forceAuthReload = false; + bool forceUpdateCheck = false; + int sampleRateIndex = 0; + int channelIndex = 0; + bool llBufferingEnabled = false; + bool hotkeysLoaded = false; + + int lastSimpleRecQualityIdx = 0; + int lastServiceIdx = -1; + int lastIgnoreRecommended = -1; + int lastChannelSetupIdx = 0; + + static constexpr uint32_t ENCODER_HIDE_FLAGS = (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL); + + OBSTheme *savedTheme = nullptr; + + std::vector formats; + + OBSPropertiesView *streamProperties = nullptr; + OBSPropertiesView *streamEncoderProps = nullptr; + OBSPropertiesView *recordEncoderProps = nullptr; + + QPointer advOutRecWarning; + QPointer simpleOutRecWarning; + + QString curPreset; + QString curQSVPreset; + QString curNVENCPreset; + QString curAMDPreset; + QString curAMDAV1Preset; + + QString curAdvStreamEncoder; + QString curAdvRecordEncoder; + + using AudioSource_t = std::tuple, QPointer, QPointer, + QPointer>; + std::vector audioSources; + std::vector audioSourceSignals; + OBSSignal sourceCreated; + OBSSignal channelChanged; + + std::vector>> hotkeys; + OBSSignal hotkeyRegistered; + OBSSignal hotkeyUnregistered; + + uint32_t outputCX = 0; + uint32_t outputCY = 0; + + QPointer simpleVodTrack; + + QPointer vodTrackCheckbox; + QPointer vodTrackContainer; + QPointer vodTrack[MAX_AUDIO_MIXES]; + + QIcon hotkeyConflictIcon; + + void SaveCombo(QComboBox *widget, const char *section, const char *value); + void SaveComboData(QComboBox *widget, const char *section, const char *value); + void SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert = false); + void SaveGroupBox(QGroupBox *widget, const char *section, const char *value); + void SaveEdit(QLineEdit *widget, const char *section, const char *value); + void SaveSpinBox(QSpinBox *widget, const char *section, const char *value); + void SaveText(QPlainTextEdit *widget, const char *section, const char *value); + void SaveFormat(QComboBox *combo); + void SaveEncoder(QComboBox *combo, const char *section, const char *value); + + bool ResFPSValid(obs_service_resolution *res_list, size_t res_count, int max_fps); + + // TODO: Remove, orphaned method + void ClosestResFPS(obs_service_resolution *res_list, size_t res_count, int max_fps, int &new_cx, int &new_cy, + int &new_fps); + + inline bool Changed() const + { + return generalChanged || appearanceChanged || outputsChanged || stream1Changed || audioChanged || + videoChanged || advancedChanged || hotkeysChanged || a11yChanged; + } + + inline void EnableApplyButton(bool en) { ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(en); } + + inline void ClearChanged() + { + generalChanged = false; + stream1Changed = false; + outputsChanged = false; + audioChanged = false; + videoChanged = false; + hotkeysChanged = false; + a11yChanged = false; + advancedChanged = false; + appearanceChanged = false; + EnableApplyButton(false); + } + + template + void HookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), + void (OBSBasicSettings::*slot)(SlotArgs...)) + { + QObject::connect(widget, signal, this, slot); + widget->setProperty("changed", QVariant(false)); + } + + bool QueryChanges(); + bool QueryAllowedToClose(); + + void ResetEncoders(bool streamOnly = false); + void LoadColorRanges(); + void LoadColorSpaces(); + void LoadColorFormats(); + void LoadFormats(); + void ReloadCodecs(const FFmpegFormat &format); + + void UpdateColorFormatSpaceWarning(); + + void LoadGeneralSettings(); + void LoadStream1Settings(); + void LoadOutputSettings(); + void LoadAudioSettings(); + void LoadVideoSettings(); + void LoadHotkeySettings(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); + void LoadA11ySettings(bool presetChange = false); + void LoadAppearanceSettings(bool reload = false); + void LoadAdvancedSettings(); + void LoadSettings(bool changedOnly); + + OBSPropertiesView *CreateEncoderPropertyView(const char *encoder, const char *path, bool changed = false); + + /* general */ + void LoadLanguageList(); + void LoadThemeList(bool firstLoad); + void LoadBranchesList(); + + /* stream */ + void InitStreamPage(); + bool IsCustomService() const; + inline bool IsWHIP() const; + void LoadServices(bool showAll); + void OnOAuthStreamKeyConnected(); + void OnAuthConnected(); + QString lastService; + QString protocol; + QString lastCustomServer; + int prevLangIndex; + bool prevBrowserAccel; + + void ServiceChanged(bool resetFields = false); + QString FindProtocol(); + void UpdateServerList(); + void UpdateKeyLink(); + void UpdateVodTrackSetting(); + void UpdateServiceRecommendations(); + void UpdateMoreInfoLink(); + void UpdateAdvNetworkGroup(); + + /* Appearance */ + void InitAppearancePage(); + + bool IsCustomServer(); + +private slots: + void UpdateMultitrackVideo(); + void RecreateOutputResolutionWidget(); + bool UpdateResFPSLimits(); + void DisplayEnforceWarning(bool checked); + void on_show_clicked(); + void on_authPwShow_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_useAuth_toggled(); + void on_server_currentIndexChanged(int index); + + void on_hotkeyFilterReset_clicked(); + void on_hotkeyFilterSearch_textChanged(const QString text); + void on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo); + +private: + /* output */ + void LoadSimpleOutputSettings(); + void LoadAdvOutputStreamingSettings(); + void LoadAdvOutputStreamingEncoderProperties(); + void LoadAdvOutputRecordingSettings(); + void LoadAdvOutputRecordingEncoderProperties(); + void LoadAdvOutputFFmpegSettings(); + void LoadAdvOutputAudioSettings(); + void SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncode = false); + + /* audio */ + void LoadListValues(QComboBox *widget, obs_property_t *prop, int index); + void LoadAudioDevices(); + void LoadAudioSources(); + + /* video */ + void LoadRendererList(); + void ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals = false); + void LoadDownscaleFilters(); + void LoadResolutionLists(); + void LoadFPSData(); + + /* a11y */ + void UpdateA11yColors(); + void SetDefaultColors(); + void ResetDefaultColors(); + QColor GetColor(uint32_t colorVal, QString label); + uint32_t preset = 0; + uint32_t selectRed = 0x0000FF; + uint32_t selectGreen = 0x00FF00; + uint32_t selectBlue = 0xFF7F00; + uint32_t mixerGreen = 0x267f26; + uint32_t mixerYellow = 0x267f7f; + uint32_t mixerRed = 0x26267f; + uint32_t mixerGreenActive = 0x4cff4c; + uint32_t mixerYellowActive = 0x4cffff; + uint32_t mixerRedActive = 0x4c4cff; + + void SaveGeneralSettings(); + void SaveStream1Settings(); + void SaveOutputSettings(); + void SaveAudioSettings(); + void SaveVideoSettings(); + void SaveHotkeySettings(); + void SaveA11ySettings(); + void SaveAppearanceSettings(); + void SaveAdvancedSettings(); + void SaveSettings(); + + void SearchHotkeys(const QString &text, obs_key_combination_t filterCombo); + + void UpdateSimpleOutStreamDelayEstimate(); + void UpdateAdvOutStreamDelayEstimate(); + + void FillSimpleRecordingValues(); + void FillAudioMonitoringDevices(); + + void RecalcOutputResPixels(const char *resText); + + bool AskIfCanCloseSettings(); + + void UpdateYouTubeAppDockSettings(); + + QIcon generalIcon; + QIcon appearanceIcon; + QIcon streamIcon; + QIcon outputIcon; + QIcon audioIcon; + QIcon videoIcon; + QIcon hotkeysIcon; + QIcon accessibilityIcon; + QIcon advancedIcon; + + QIcon GetGeneralIcon() const; + QIcon GetAppearanceIcon() const; + QIcon GetStreamIcon() const; + QIcon GetOutputIcon() const; + QIcon GetAudioIcon() const; + QIcon GetVideoIcon() const; + QIcon GetHotkeysIcon() const; + QIcon GetAccessibilityIcon() const; + QIcon GetAdvancedIcon() const; + + int CurrentFLVTrack(); + int SimpleOutGetSelectedAudioTracks(); + int AdvOutGetSelectedAudioTracks(); + int AdvOutGetStreamingSelectedAudioTracks(); + + OBSService GetStream1Service(); + + bool ServiceAndVCodecCompatible(); + bool ServiceAndACodecCompatible(); + bool ServiceSupportsCodecCheck(); + + inline bool AllowsMultiTrack(const char *protocol); + void SwapMultiTrack(const char *protocol); + +private slots: + void on_theme_activated(int idx); + void on_themeVariant_activated(int idx); + + void on_listWidget_itemSelectionChanged(); + void on_buttonBox_clicked(QAbstractButton *button); + + void on_service_currentIndexChanged(int idx); + void on_customServer_textChanged(const QString &text); + void on_simpleOutputBrowse_clicked(); + void on_advOutRecPathBrowse_clicked(); + void on_advOutFFPathBrowse_clicked(); + void on_advOutEncoder_currentIndexChanged(); + void on_advOutRecEncoder_currentIndexChanged(int idx); + void on_advOutFFIgnoreCompat_stateChanged(int state); + void on_advOutFFFormat_currentIndexChanged(int idx); + void on_advOutFFAEncoder_currentIndexChanged(int idx); + void on_advOutFFVEncoder_currentIndexChanged(int idx); + void on_advOutFFType_currentIndexChanged(int idx); + + void on_colorFormat_currentIndexChanged(int idx); + void on_colorSpace_currentIndexChanged(int idx); + + void on_filenameFormatting_textEdited(const QString &text); + void on_outputResolution_editTextChanged(const QString &text); + void on_baseResolution_editTextChanged(const QString &text); + + void on_disableOSXVSync_clicked(); + + void on_choose1_clicked(); + void on_choose2_clicked(); + void on_choose3_clicked(); + void on_choose4_clicked(); + void on_choose5_clicked(); + void on_choose6_clicked(); + void on_choose7_clicked(); + void on_choose8_clicked(); + void on_choose9_clicked(); + void on_colorPreset_currentIndexChanged(int idx); + + void GeneralChanged(); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + void HideOBSWindowWarning(Qt::CheckState state); +#else + void HideOBSWindowWarning(int state); +#endif + void AudioChanged(); + void AudioChangedRestart(); + void ReloadAudioSources(); + void SurroundWarning(int idx); + void SpeakerLayoutChanged(int idx); + void LowLatencyBufferingChanged(bool checked); + void UpdateAudioWarnings(); + void OutputsChanged(); + void Stream1Changed(); + void VideoChanged(); + void VideoChangedResolution(); + void HotkeysChanged(); + bool ScanDuplicateHotkeys(QFormLayout *layout); + void ReloadHotkeys(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); + void A11yChanged(); + void AppearanceChanged(); + void AdvancedChanged(); + void AdvancedChangedRestart(); + + void UpdateStreamDelayEstimate(); + + void UpdateAutomaticReplayBufferCheckboxes(); + + void AdvOutSplitFileChanged(); + void AdvOutRecCheckWarnings(); + void AdvOutRecCheckCodecs(); + + void SimpleRecordingQualityChanged(); + void SimpleRecordingEncoderChanged(); + void SimpleRecordingQualityLosslessWarning(int idx); + + void SimpleReplayBufferChanged(); + void AdvReplayBufferChanged(); + + void SimpleStreamingEncoderChanged(); + + OBSService SpawnTempService(); + + void SetGeneralIcon(const QIcon &icon); + void SetAppearanceIcon(const QIcon &icon); + void SetStreamIcon(const QIcon &icon); + void SetOutputIcon(const QIcon &icon); + void SetAudioIcon(const QIcon &icon); + void SetVideoIcon(const QIcon &icon); + void SetHotkeysIcon(const QIcon &icon); + void SetAccessibilityIcon(const QIcon &icon); + void SetAdvancedIcon(const QIcon &icon); + + void UseStreamKeyAdvClicked(); + + void SimpleStreamAudioEncoderChanged(); + void AdvAudioEncodersChanged(); + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual void showEvent(QShowEvent *event) override; + void reject() override; + +public: + OBSBasicSettings(QWidget *parent); + ~OBSBasicSettings(); + + inline const QIcon &GetHotkeyConflictIcon() const { return hotkeyConflictIcon; } +}; diff --git a/UI/hotkey-edit.cpp b/frontend/settings/OBSHotkeyEdit.cpp similarity index 100% rename from UI/hotkey-edit.cpp rename to frontend/settings/OBSHotkeyEdit.cpp diff --git a/UI/hotkey-edit.hpp b/frontend/settings/OBSHotkeyEdit.hpp similarity index 100% rename from UI/hotkey-edit.hpp rename to frontend/settings/OBSHotkeyEdit.hpp diff --git a/frontend/settings/OBSHotkeyLabel.cpp b/frontend/settings/OBSHotkeyLabel.cpp new file mode 100644 index 000000000..5dc5355e7 --- /dev/null +++ b/frontend/settings/OBSHotkeyLabel.cpp @@ -0,0 +1,470 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + 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 "window-basic-settings.hpp" +#include "moc_hotkey-edit.cpp" + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" + +void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + obs_key_combination_t new_key; + + switch (event->key()) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + new_key.key = OBS_KEY_NONE; + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + break; +#endif + + default: + new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const +{ + if (query == Qt::ImEnabled) { + return false; + } else { + return QLineEdit::inputMethodQuery(query); + } +} + +#ifdef __APPLE__ +void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + if (event->key() != Qt::Key_CapsLock) + return; + + obs_key_combination_t new_key; + + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} +#endif + +void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) +{ + obs_key_combination_t new_key; + + switch (event->button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return; + + case Qt::MiddleButton: + new_key.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + new_key.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4) + MAP_BUTTON(2, 5) + MAP_BUTTON(3, 6) + MAP_BUTTON(4, 7) + MAP_BUTTON(5, 8) + MAP_BUTTON(6, 9) + MAP_BUTTON(7, 10) + MAP_BUTTON(8, 11) + MAP_BUTTON(9, 12) + MAP_BUTTON(10, 13) + MAP_BUTTON(11, 14) + MAP_BUTTON(12, 15) + MAP_BUTTON(13, 16) + MAP_BUTTON(14, 17) + MAP_BUTTON(15, 18) + MAP_BUTTON(16, 19) + MAP_BUTTON(17, 20) + MAP_BUTTON(18, 21) + MAP_BUTTON(19, 22) + MAP_BUTTON(20, 23) + MAP_BUTTON(21, 24) + MAP_BUTTON(22, 25) + MAP_BUTTON(23, 26) + MAP_BUTTON(24, 27) +#undef MAP_BUTTON + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) +{ + if (new_key == key || obs_key_combination_is_empty(new_key)) + return; + + key = new_key; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::RenderKey() +{ + DStr str; + obs_key_combination_to_str(key, str); + + setText(QT_UTF8(str)); +} + +void OBSHotkeyEdit::ResetKey() +{ + key = original; + + changed = false; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::ClearKey() +{ + key = {0, OBS_KEY_NONE}; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::UpdateDuplicationState() +{ + if (!dupeIcon && !hasDuplicate) + return; + + if (!dupeIcon) + CreateDupeIcon(); + + if (dupeIcon->isVisible() != hasDuplicate) { + dupeIcon->setVisible(hasDuplicate); + update(); + } +} + +void OBSHotkeyEdit::InitSignalHandler() +{ + layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", + [](void *this_, calldata_t *) { + auto edit = static_cast(this_); + QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); + }, + this}; +} + +void OBSHotkeyEdit::CreateDupeIcon() +{ + dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); + dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); + QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); + dupeIcon->setVisible(false); +} + +void OBSHotkeyEdit::ReloadKeyLayout() +{ + RenderKey(); +} + +void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) +{ + if (combos.empty()) + AddEdit({0, OBS_KEY_NONE}); + + for (auto combo : combos) + AddEdit(combo); +} + +bool OBSHotkeyWidget::Changed() const +{ + return changed || std::any_of(begin(edits), end(edits), [](OBSHotkeyEdit *edit) { return edit->changed; }); +} + +void OBSHotkeyWidget::Apply() +{ + for (auto &edit : edits) { + edit->original = edit->key; + edit->changed = false; + } + + changed = false; + + for (auto &revertButton : revertButtons) + revertButton->setEnabled(false); +} + +void OBSHotkeyWidget::GetCombinations(std::vector &combinations) const +{ + combinations.clear(); + for (auto &edit : edits) + if (!obs_key_combination_is_empty(edit->key)) + combinations.emplace_back(edit->key); +} + +void OBSHotkeyWidget::Save() +{ + std::vector combinations; + Save(combinations); +} + +void OBSHotkeyWidget::Save(std::vector &combinations) +{ + GetCombinations(combinations); + Apply(); + + auto AtomicUpdate = [&]() { + ignoreChangedBindings = true; + + obs_hotkey_load_bindings(id, combinations.data(), combinations.size()); + + ignoreChangedBindings = false; + }; + using AtomicUpdate_t = decltype(&AtomicUpdate); + + obs_hotkey_update_atomic([](void *d) { (*static_cast(d))(); }, + static_cast(&AtomicUpdate)); +} + +void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx) +{ + auto edit = new OBSHotkeyEdit(parentWidget(), combo, settings); + edit->setToolTip(toolTip); + + auto revert = new QPushButton; + revert->setProperty("class", "icon-revert"); + revert->setToolTip(QTStr("Revert")); + revert->setEnabled(false); + + auto clear = new QPushButton; + clear->setProperty("class", "icon-clear"); + clear->setToolTip(QTStr("Clear")); + clear->setEnabled(!obs_key_combination_is_empty(combo)); + + QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [=](obs_key_combination_t new_combo) { + clear->setEnabled(!obs_key_combination_is_empty(new_combo)); + revert->setEnabled(edit->original != new_combo); + }); + + auto add = new QPushButton; + add->setProperty("class", "icon-plus"); + add->setToolTip(QTStr("Add")); + + auto remove = new QPushButton; + remove->setProperty("class", "icon-trash"); + remove->setToolTip(QTStr("Remove")); + remove->setEnabled(removeButtons.size() > 0); + + auto CurrentIndex = [&, remove] { + auto res = std::find(begin(removeButtons), end(removeButtons), remove); + return std::distance(begin(removeButtons), res); + }; + + QObject::connect(add, &QPushButton::clicked, [&, CurrentIndex] { + AddEdit({0, OBS_KEY_NONE}, CurrentIndex() + 1); + }); + + QObject::connect(remove, &QPushButton::clicked, [&, CurrentIndex] { RemoveEdit(CurrentIndex()); }); + + QHBoxLayout *subLayout = new QHBoxLayout; + subLayout->setContentsMargins(0, 2, 0, 2); + subLayout->addWidget(edit); + subLayout->addWidget(revert); + subLayout->addWidget(clear); + subLayout->addWidget(add); + subLayout->addWidget(remove); + + if (removeButtons.size() == 1) + removeButtons.front()->setEnabled(true); + + if (idx != -1) { + revertButtons.insert(begin(revertButtons) + idx, revert); + removeButtons.insert(begin(removeButtons) + idx, remove); + edits.insert(begin(edits) + idx, edit); + } else { + revertButtons.emplace_back(revert); + removeButtons.emplace_back(remove); + edits.emplace_back(edit); + } + + layout()->insertLayout(idx, subLayout); + + QObject::connect(revert, &QPushButton::clicked, edit, &OBSHotkeyEdit::ResetKey); + QObject::connect(clear, &QPushButton::clicked, edit, &OBSHotkeyEdit::ClearKey); + + QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [&](obs_key_combination) { emit KeyChanged(); }); + QObject::connect(edit, &OBSHotkeyEdit::SearchKey, [=](obs_key_combination combo) { emit SearchKey(combo); }); +} + +void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal) +{ + auto &edit = *(begin(edits) + idx); + if (!obs_key_combination_is_empty(edit->original) && signal) { + changed = true; + } + + revertButtons.erase(begin(revertButtons) + idx); + removeButtons.erase(begin(removeButtons) + idx); + edits.erase(begin(edits) + idx); + + auto item = layout()->takeAt(static_cast(idx)); + QLayoutItem *child = nullptr; + while ((child = item->layout()->takeAt(0))) { + delete child->widget(); + delete child; + } + delete item; + + if (removeButtons.size() == 1) + removeButtons.front()->setEnabled(false); + + emit KeyChanged(); +} + +void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param) +{ + auto widget = static_cast(data); + auto key = static_cast(calldata_ptr(param, "key")); + + QMetaObject::invokeMethod(widget, "HandleChangedBindings", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); +} + +void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) +{ + if (ignoreChangedBindings || id != id_) + return; + + std::vector bindings; + auto LoadBindings = [&](obs_hotkey_binding_t *binding) { + if (obs_hotkey_binding_get_hotkey_id(binding) != id) + return; + + auto get_combo = obs_hotkey_binding_get_key_combination; + bindings.push_back(get_combo(binding)); + }; + using LoadBindings_t = decltype(&LoadBindings); + + obs_enum_hotkey_bindings( + [](void *data, size_t, obs_hotkey_binding_t *binding) { + auto LoadBindings = *static_cast(data); + LoadBindings(binding); + return true; + }, + static_cast(&LoadBindings)); + + while (edits.size() > 0) + RemoveEdit(edits.size() - 1, false); + + SetKeyCombinations(bindings); +} + +static inline void updateStyle(QWidget *widget) +{ + auto style = widget->style(); + style->unpolish(widget); + style->polish(widget); + widget->update(); +} + +void OBSHotkeyWidget::enterEvent(QEnterEvent *event) +{ + if (!label) + return; + + event->accept(); + label->highlightPair(true); +} + +void OBSHotkeyWidget::leaveEvent(QEvent *event) +{ + if (!label) + return; + + event->accept(); + label->highlightPair(false); +} + +void OBSHotkeyLabel::highlightPair(bool highlight) +{ + if (!pairPartner) + return; + + pairPartner->setProperty("class", highlight ? "text-bright" : ""); + updateStyle(pairPartner); + setProperty("class", highlight ? "text-bright" : ""); + updateStyle(this); +} + +void OBSHotkeyLabel::enterEvent(QEnterEvent *event) +{ + + if (!pairPartner) + return; + + event->accept(); + highlightPair(true); +} + +void OBSHotkeyLabel::leaveEvent(QEvent *event) +{ + if (!pairPartner) + return; + + event->accept(); + highlightPair(false); +} + +void OBSHotkeyLabel::setToolTip(const QString &toolTip) +{ + QLabel::setToolTip(toolTip); + if (widget) + widget->setToolTip(toolTip); +} diff --git a/frontend/settings/OBSHotkeyLabel.hpp b/frontend/settings/OBSHotkeyLabel.hpp new file mode 100644 index 000000000..ea7e9b4ae --- /dev/null +++ b/frontend/settings/OBSHotkeyLabel.hpp @@ -0,0 +1,190 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + 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 +#include +#include +#include +#include +#include +#include + +#include + +static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) +{ + return c1.modifiers != c2.modifiers || c1.key != c2.key; +} + +static inline bool operator==(const obs_key_combination_t &c1, const obs_key_combination_t &c2) +{ + return !(c1 != c2); +} + +class OBSBasicSettings; +class OBSHotkeyWidget; + +class OBSHotkeyLabel : public QLabel { + Q_OBJECT + +public: + QPointer pairPartner; + QPointer widget; + void highlightPair(bool highlight); + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void setToolTip(const QString &toolTip); +}; + +class OBSHotkeyEdit : public QLineEdit { + Q_OBJECT; + +public: + OBSHotkeyEdit(QWidget *parent, obs_key_combination_t original, OBSBasicSettings *settings) + : QLineEdit(parent), + original(original), + settings(settings) + { +#ifdef __APPLE__ + // disable the input cursor on OSX, focus should be clear + // enough with the default focus frame + setReadOnly(true); +#endif + setAttribute(Qt::WA_InputMethodEnabled, false); + setAttribute(Qt::WA_MacShowFocusRect, true); + InitSignalHandler(); + ResetKey(); + } + OBSHotkeyEdit(QWidget *parent = nullptr) : QLineEdit(parent), original({}), settings(nullptr) + { +#ifdef __APPLE__ + // disable the input cursor on OSX, focus should be clear + // enough with the default focus frame + setReadOnly(true); +#endif + setAttribute(Qt::WA_InputMethodEnabled, false); + setAttribute(Qt::WA_MacShowFocusRect, true); + InitSignalHandler(); + ResetKey(); + } + + obs_key_combination_t original; + obs_key_combination_t key; + OBSBasicSettings *settings; + bool changed = false; + + void UpdateDuplicationState(); + bool hasDuplicate = false; + QVariant inputMethodQuery(Qt::InputMethodQuery) const override; + +protected: + OBSSignal layoutChanged; + QAction *dupeIcon = nullptr; + + void InitSignalHandler(); + void CreateDupeIcon(); + + void keyPressEvent(QKeyEvent *event) override; +#ifdef __APPLE__ + void keyReleaseEvent(QKeyEvent *event) override; +#endif + void mousePressEvent(QMouseEvent *event) override; + + void RenderKey(); + +public slots: + void HandleNewKey(obs_key_combination_t new_key); + void ReloadKeyLayout(); + void ResetKey(); + void ClearKey(); + +signals: + void KeyChanged(obs_key_combination_t); + void SearchKey(obs_key_combination_t); +}; + +class OBSHotkeyWidget : public QWidget { + Q_OBJECT; + +public: + OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, + const std::vector &combos = {}) + : QWidget(parent), + id(id), + name(name), + bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", + &OBSHotkeyWidget::BindingsChanged, this), + settings(settings) + { + auto layout = new QVBoxLayout; + layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + SetKeyCombinations(combos); + } + + void SetKeyCombinations(const std::vector &); + + obs_hotkey_id id; + std::string name; + + bool changed = false; + bool Changed() const; + + QPointer label; + std::vector> edits; + + QString toolTip; + void setToolTip(const QString &toolTip_) + { + toolTip = toolTip_; + for (auto &edit : edits) + edit->setToolTip(toolTip_); + } + + void Apply(); + void GetCombinations(std::vector &) const; + void Save(); + void Save(std::vector &combinations); + + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + void AddEdit(obs_key_combination combo, int idx = -1); + void RemoveEdit(size_t idx, bool signal = true); + + static void BindingsChanged(void *data, calldata_t *param); + + std::vector> removeButtons; + std::vector> revertButtons; + OBSSignal bindingsChanged; + bool ignoreChangedBindings = false; + OBSBasicSettings *settings; + + QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } + +private slots: + void HandleChangedBindings(obs_hotkey_id id_); + +signals: + void KeyChanged(); + void SearchKey(obs_key_combination_t); +}; diff --git a/frontend/settings/OBSHotkeyWidget.cpp b/frontend/settings/OBSHotkeyWidget.cpp new file mode 100644 index 000000000..5dc5355e7 --- /dev/null +++ b/frontend/settings/OBSHotkeyWidget.cpp @@ -0,0 +1,470 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + 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 "window-basic-settings.hpp" +#include "moc_hotkey-edit.cpp" + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" + +void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + obs_key_combination_t new_key; + + switch (event->key()) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + new_key.key = OBS_KEY_NONE; + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + break; +#endif + + default: + new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const +{ + if (query == Qt::ImEnabled) { + return false; + } else { + return QLineEdit::inputMethodQuery(query); + } +} + +#ifdef __APPLE__ +void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + if (event->key() != Qt::Key_CapsLock) + return; + + obs_key_combination_t new_key; + + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} +#endif + +void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) +{ + obs_key_combination_t new_key; + + switch (event->button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return; + + case Qt::MiddleButton: + new_key.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + new_key.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4) + MAP_BUTTON(2, 5) + MAP_BUTTON(3, 6) + MAP_BUTTON(4, 7) + MAP_BUTTON(5, 8) + MAP_BUTTON(6, 9) + MAP_BUTTON(7, 10) + MAP_BUTTON(8, 11) + MAP_BUTTON(9, 12) + MAP_BUTTON(10, 13) + MAP_BUTTON(11, 14) + MAP_BUTTON(12, 15) + MAP_BUTTON(13, 16) + MAP_BUTTON(14, 17) + MAP_BUTTON(15, 18) + MAP_BUTTON(16, 19) + MAP_BUTTON(17, 20) + MAP_BUTTON(18, 21) + MAP_BUTTON(19, 22) + MAP_BUTTON(20, 23) + MAP_BUTTON(21, 24) + MAP_BUTTON(22, 25) + MAP_BUTTON(23, 26) + MAP_BUTTON(24, 27) +#undef MAP_BUTTON + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) +{ + if (new_key == key || obs_key_combination_is_empty(new_key)) + return; + + key = new_key; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::RenderKey() +{ + DStr str; + obs_key_combination_to_str(key, str); + + setText(QT_UTF8(str)); +} + +void OBSHotkeyEdit::ResetKey() +{ + key = original; + + changed = false; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::ClearKey() +{ + key = {0, OBS_KEY_NONE}; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::UpdateDuplicationState() +{ + if (!dupeIcon && !hasDuplicate) + return; + + if (!dupeIcon) + CreateDupeIcon(); + + if (dupeIcon->isVisible() != hasDuplicate) { + dupeIcon->setVisible(hasDuplicate); + update(); + } +} + +void OBSHotkeyEdit::InitSignalHandler() +{ + layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", + [](void *this_, calldata_t *) { + auto edit = static_cast(this_); + QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); + }, + this}; +} + +void OBSHotkeyEdit::CreateDupeIcon() +{ + dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); + dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); + QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); + dupeIcon->setVisible(false); +} + +void OBSHotkeyEdit::ReloadKeyLayout() +{ + RenderKey(); +} + +void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) +{ + if (combos.empty()) + AddEdit({0, OBS_KEY_NONE}); + + for (auto combo : combos) + AddEdit(combo); +} + +bool OBSHotkeyWidget::Changed() const +{ + return changed || std::any_of(begin(edits), end(edits), [](OBSHotkeyEdit *edit) { return edit->changed; }); +} + +void OBSHotkeyWidget::Apply() +{ + for (auto &edit : edits) { + edit->original = edit->key; + edit->changed = false; + } + + changed = false; + + for (auto &revertButton : revertButtons) + revertButton->setEnabled(false); +} + +void OBSHotkeyWidget::GetCombinations(std::vector &combinations) const +{ + combinations.clear(); + for (auto &edit : edits) + if (!obs_key_combination_is_empty(edit->key)) + combinations.emplace_back(edit->key); +} + +void OBSHotkeyWidget::Save() +{ + std::vector combinations; + Save(combinations); +} + +void OBSHotkeyWidget::Save(std::vector &combinations) +{ + GetCombinations(combinations); + Apply(); + + auto AtomicUpdate = [&]() { + ignoreChangedBindings = true; + + obs_hotkey_load_bindings(id, combinations.data(), combinations.size()); + + ignoreChangedBindings = false; + }; + using AtomicUpdate_t = decltype(&AtomicUpdate); + + obs_hotkey_update_atomic([](void *d) { (*static_cast(d))(); }, + static_cast(&AtomicUpdate)); +} + +void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx) +{ + auto edit = new OBSHotkeyEdit(parentWidget(), combo, settings); + edit->setToolTip(toolTip); + + auto revert = new QPushButton; + revert->setProperty("class", "icon-revert"); + revert->setToolTip(QTStr("Revert")); + revert->setEnabled(false); + + auto clear = new QPushButton; + clear->setProperty("class", "icon-clear"); + clear->setToolTip(QTStr("Clear")); + clear->setEnabled(!obs_key_combination_is_empty(combo)); + + QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [=](obs_key_combination_t new_combo) { + clear->setEnabled(!obs_key_combination_is_empty(new_combo)); + revert->setEnabled(edit->original != new_combo); + }); + + auto add = new QPushButton; + add->setProperty("class", "icon-plus"); + add->setToolTip(QTStr("Add")); + + auto remove = new QPushButton; + remove->setProperty("class", "icon-trash"); + remove->setToolTip(QTStr("Remove")); + remove->setEnabled(removeButtons.size() > 0); + + auto CurrentIndex = [&, remove] { + auto res = std::find(begin(removeButtons), end(removeButtons), remove); + return std::distance(begin(removeButtons), res); + }; + + QObject::connect(add, &QPushButton::clicked, [&, CurrentIndex] { + AddEdit({0, OBS_KEY_NONE}, CurrentIndex() + 1); + }); + + QObject::connect(remove, &QPushButton::clicked, [&, CurrentIndex] { RemoveEdit(CurrentIndex()); }); + + QHBoxLayout *subLayout = new QHBoxLayout; + subLayout->setContentsMargins(0, 2, 0, 2); + subLayout->addWidget(edit); + subLayout->addWidget(revert); + subLayout->addWidget(clear); + subLayout->addWidget(add); + subLayout->addWidget(remove); + + if (removeButtons.size() == 1) + removeButtons.front()->setEnabled(true); + + if (idx != -1) { + revertButtons.insert(begin(revertButtons) + idx, revert); + removeButtons.insert(begin(removeButtons) + idx, remove); + edits.insert(begin(edits) + idx, edit); + } else { + revertButtons.emplace_back(revert); + removeButtons.emplace_back(remove); + edits.emplace_back(edit); + } + + layout()->insertLayout(idx, subLayout); + + QObject::connect(revert, &QPushButton::clicked, edit, &OBSHotkeyEdit::ResetKey); + QObject::connect(clear, &QPushButton::clicked, edit, &OBSHotkeyEdit::ClearKey); + + QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [&](obs_key_combination) { emit KeyChanged(); }); + QObject::connect(edit, &OBSHotkeyEdit::SearchKey, [=](obs_key_combination combo) { emit SearchKey(combo); }); +} + +void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal) +{ + auto &edit = *(begin(edits) + idx); + if (!obs_key_combination_is_empty(edit->original) && signal) { + changed = true; + } + + revertButtons.erase(begin(revertButtons) + idx); + removeButtons.erase(begin(removeButtons) + idx); + edits.erase(begin(edits) + idx); + + auto item = layout()->takeAt(static_cast(idx)); + QLayoutItem *child = nullptr; + while ((child = item->layout()->takeAt(0))) { + delete child->widget(); + delete child; + } + delete item; + + if (removeButtons.size() == 1) + removeButtons.front()->setEnabled(false); + + emit KeyChanged(); +} + +void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param) +{ + auto widget = static_cast(data); + auto key = static_cast(calldata_ptr(param, "key")); + + QMetaObject::invokeMethod(widget, "HandleChangedBindings", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); +} + +void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) +{ + if (ignoreChangedBindings || id != id_) + return; + + std::vector bindings; + auto LoadBindings = [&](obs_hotkey_binding_t *binding) { + if (obs_hotkey_binding_get_hotkey_id(binding) != id) + return; + + auto get_combo = obs_hotkey_binding_get_key_combination; + bindings.push_back(get_combo(binding)); + }; + using LoadBindings_t = decltype(&LoadBindings); + + obs_enum_hotkey_bindings( + [](void *data, size_t, obs_hotkey_binding_t *binding) { + auto LoadBindings = *static_cast(data); + LoadBindings(binding); + return true; + }, + static_cast(&LoadBindings)); + + while (edits.size() > 0) + RemoveEdit(edits.size() - 1, false); + + SetKeyCombinations(bindings); +} + +static inline void updateStyle(QWidget *widget) +{ + auto style = widget->style(); + style->unpolish(widget); + style->polish(widget); + widget->update(); +} + +void OBSHotkeyWidget::enterEvent(QEnterEvent *event) +{ + if (!label) + return; + + event->accept(); + label->highlightPair(true); +} + +void OBSHotkeyWidget::leaveEvent(QEvent *event) +{ + if (!label) + return; + + event->accept(); + label->highlightPair(false); +} + +void OBSHotkeyLabel::highlightPair(bool highlight) +{ + if (!pairPartner) + return; + + pairPartner->setProperty("class", highlight ? "text-bright" : ""); + updateStyle(pairPartner); + setProperty("class", highlight ? "text-bright" : ""); + updateStyle(this); +} + +void OBSHotkeyLabel::enterEvent(QEnterEvent *event) +{ + + if (!pairPartner) + return; + + event->accept(); + highlightPair(true); +} + +void OBSHotkeyLabel::leaveEvent(QEvent *event) +{ + if (!pairPartner) + return; + + event->accept(); + highlightPair(false); +} + +void OBSHotkeyLabel::setToolTip(const QString &toolTip) +{ + QLabel::setToolTip(toolTip); + if (widget) + widget->setToolTip(toolTip); +} diff --git a/frontend/settings/OBSHotkeyWidget.hpp b/frontend/settings/OBSHotkeyWidget.hpp new file mode 100644 index 000000000..ea7e9b4ae --- /dev/null +++ b/frontend/settings/OBSHotkeyWidget.hpp @@ -0,0 +1,190 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + 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 +#include +#include +#include +#include +#include +#include + +#include + +static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) +{ + return c1.modifiers != c2.modifiers || c1.key != c2.key; +} + +static inline bool operator==(const obs_key_combination_t &c1, const obs_key_combination_t &c2) +{ + return !(c1 != c2); +} + +class OBSBasicSettings; +class OBSHotkeyWidget; + +class OBSHotkeyLabel : public QLabel { + Q_OBJECT + +public: + QPointer pairPartner; + QPointer widget; + void highlightPair(bool highlight); + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void setToolTip(const QString &toolTip); +}; + +class OBSHotkeyEdit : public QLineEdit { + Q_OBJECT; + +public: + OBSHotkeyEdit(QWidget *parent, obs_key_combination_t original, OBSBasicSettings *settings) + : QLineEdit(parent), + original(original), + settings(settings) + { +#ifdef __APPLE__ + // disable the input cursor on OSX, focus should be clear + // enough with the default focus frame + setReadOnly(true); +#endif + setAttribute(Qt::WA_InputMethodEnabled, false); + setAttribute(Qt::WA_MacShowFocusRect, true); + InitSignalHandler(); + ResetKey(); + } + OBSHotkeyEdit(QWidget *parent = nullptr) : QLineEdit(parent), original({}), settings(nullptr) + { +#ifdef __APPLE__ + // disable the input cursor on OSX, focus should be clear + // enough with the default focus frame + setReadOnly(true); +#endif + setAttribute(Qt::WA_InputMethodEnabled, false); + setAttribute(Qt::WA_MacShowFocusRect, true); + InitSignalHandler(); + ResetKey(); + } + + obs_key_combination_t original; + obs_key_combination_t key; + OBSBasicSettings *settings; + bool changed = false; + + void UpdateDuplicationState(); + bool hasDuplicate = false; + QVariant inputMethodQuery(Qt::InputMethodQuery) const override; + +protected: + OBSSignal layoutChanged; + QAction *dupeIcon = nullptr; + + void InitSignalHandler(); + void CreateDupeIcon(); + + void keyPressEvent(QKeyEvent *event) override; +#ifdef __APPLE__ + void keyReleaseEvent(QKeyEvent *event) override; +#endif + void mousePressEvent(QMouseEvent *event) override; + + void RenderKey(); + +public slots: + void HandleNewKey(obs_key_combination_t new_key); + void ReloadKeyLayout(); + void ResetKey(); + void ClearKey(); + +signals: + void KeyChanged(obs_key_combination_t); + void SearchKey(obs_key_combination_t); +}; + +class OBSHotkeyWidget : public QWidget { + Q_OBJECT; + +public: + OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, + const std::vector &combos = {}) + : QWidget(parent), + id(id), + name(name), + bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", + &OBSHotkeyWidget::BindingsChanged, this), + settings(settings) + { + auto layout = new QVBoxLayout; + layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + SetKeyCombinations(combos); + } + + void SetKeyCombinations(const std::vector &); + + obs_hotkey_id id; + std::string name; + + bool changed = false; + bool Changed() const; + + QPointer label; + std::vector> edits; + + QString toolTip; + void setToolTip(const QString &toolTip_) + { + toolTip = toolTip_; + for (auto &edit : edits) + edit->setToolTip(toolTip_); + } + + void Apply(); + void GetCombinations(std::vector &) const; + void Save(); + void Save(std::vector &combinations); + + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + void AddEdit(obs_key_combination combo, int idx = -1); + void RemoveEdit(size_t idx, bool signal = true); + + static void BindingsChanged(void *data, calldata_t *param); + + std::vector> removeButtons; + std::vector> revertButtons; + OBSSignal bindingsChanged; + bool ignoreChangedBindings = false; + OBSBasicSettings *settings; + + QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } + +private slots: + void HandleChangedBindings(obs_hotkey_id id_); + +signals: + void KeyChanged(); + void SearchKey(obs_key_combination_t); +}; diff --git a/frontend/utility/SettingsEventFilter.hpp b/frontend/utility/SettingsEventFilter.hpp new file mode 100644 index 000000000..d8a35501b --- /dev/null +++ b/frontend/utility/SettingsEventFilter.hpp @@ -0,0 +1,5825 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audio-encoders.hpp" +#include "hotkey-edit.hpp" +#include "source-label.hpp" +#include "obs-app.hpp" +#include "platform.hpp" +#include "properties-view.hpp" +#include "window-basic-main.hpp" +#include "moc_window-basic-settings.cpp" +#include "window-basic-main-outputs.hpp" +#include "window-projector.hpp" + +#ifdef YOUTUBE_ENABLED +#include "youtube-api-wrappers.hpp" +#endif + +#include +#include +#include "ui-config.h" + +using namespace std; + +class SettingsEventFilter : public QObject { + QScopedPointer shortcutFilter; + +public: + inline SettingsEventFilter() : shortcutFilter((OBSEventFilter *)CreateShortcutFilter()) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) override + { + int key; + + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + key = static_cast(event)->key(); + if (key == Qt::Key_Escape) { + return false; + } + default: + break; + } + + return shortcutFilter->filter(obj, event); + } +}; + +static inline bool ResTooHigh(uint32_t cx, uint32_t cy) +{ + return cx > 16384 || cy > 16384; +} + +static inline bool ResTooLow(uint32_t cx, uint32_t cy) +{ + return cx < 32 || cy < 32; +} + +/* parses "[width]x[height]", string, i.e. 1024x768 */ +static bool ConvertResText(const char *res, uint32_t &cx, uint32_t &cy) +{ + BaseLexer lex; + base_token token; + + lexer_start(lex, res); + + /* parse width */ + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != BASETOKEN_DIGIT) + return false; + + cx = std::stoul(token.text.array); + + /* parse 'x' */ + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (strref_cmpi(&token.text, "x") != 0) + return false; + + /* parse height */ + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != BASETOKEN_DIGIT) + return false; + + cy = std::stoul(token.text.array); + + /* shouldn't be any more tokens after this */ + if (lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + + if (ResTooHigh(cx, cy) || ResTooLow(cx, cy)) { + cx = cy = 0; + return false; + } + + return true; +} + +static inline bool WidgetChanged(QWidget *widget) +{ + return widget->property("changed").toBool(); +} + +static inline void SetComboByName(QComboBox *combo, const char *name) +{ + int idx = combo->findText(QT_UTF8(name)); + if (idx != -1) + combo->setCurrentIndex(idx); +} + +static inline bool SetComboByValue(QComboBox *combo, const char *name) +{ + int idx = combo->findData(QT_UTF8(name)); + if (idx != -1) { + combo->setCurrentIndex(idx); + return true; + } + + return false; +} + +static inline bool SetInvalidValue(QComboBox *combo, const char *name, const char *data = nullptr) +{ + combo->insertItem(0, name, data); + + QStandardItemModel *model = dynamic_cast(combo->model()); + if (!model) + return false; + + QStandardItem *item = model->item(0); + item->setFlags(Qt::NoItemFlags); + + combo->setCurrentIndex(0); + return true; +} + +static inline QString GetComboData(QComboBox *combo) +{ + int idx = combo->currentIndex(); + if (idx == -1) + return QString(); + + return combo->itemData(idx).toString(); +} + +static int FindEncoder(QComboBox *combo, const char *name, int id) +{ + FFmpegCodec codec{name, id}; + + for (int i = 0; i < combo->count(); i++) { + QVariant v = combo->itemData(i); + if (!v.isNull()) { + if (codec == v.value()) { + return i; + } + } + } + return -1; +} + +#define INVALID_BITRATE 10000 +static int FindClosestAvailableAudioBitrate(QComboBox *box, int bitrate) +{ + QList bitrates; + int prev = 0; + int next = INVALID_BITRATE; + + for (int i = 0; i < box->count(); i++) + bitrates << box->itemText(i).toInt(); + + for (int val : bitrates) { + if (next > val) { + if (val == bitrate) + return bitrate; + + if (val < next && val > bitrate) + next = val; + if (val > prev && val < bitrate) + prev = val; + } + } + + if (next != INVALID_BITRATE) + return next; + if (prev != 0) + return prev; + return 192; +} +#undef INVALID_BITRATE + +static void PopulateSimpleBitrates(QComboBox *box, bool opus) +{ + auto &bitrateMap = opus ? GetSimpleOpusEncoderBitrateMap() : GetSimpleAACEncoderBitrateMap(); + if (bitrateMap.empty()) + return; + + vector> pairs; + for (auto &entry : bitrateMap) + pairs.emplace_back(QString::number(entry.first), obs_encoder_get_display_name(entry.second.c_str())); + + QString currentBitrate = box->currentText(); + box->clear(); + + for (auto &pair : pairs) { + box->addItem(pair.first); + box->setItemData(box->count() - 1, pair.second, Qt::ToolTipRole); + } + + if (box->findData(currentBitrate) == -1) { + int bitrate = FindClosestAvailableAudioBitrate(box, currentBitrate.toInt()); + box->setCurrentText(QString::number(bitrate)); + } else + box->setCurrentText(currentBitrate); +} + +static void PopulateAdvancedBitrates(initializer_list boxes, const char *stream_id, const char *rec_id) +{ + auto &streamBitrates = GetAudioEncoderBitrates(stream_id); + auto &recBitrates = GetAudioEncoderBitrates(rec_id); + if (streamBitrates.empty() || recBitrates.empty()) + return; + + QList streamBitratesList; + for (auto &bitrate : streamBitrates) + streamBitratesList << bitrate; + + for (auto box : boxes) { + QString currentBitrate = box->currentText(); + box->clear(); + + for (auto &bitrate : recBitrates) { + if (streamBitratesList.indexOf(bitrate) == -1) + continue; + + box->addItem(QString::number(bitrate)); + } + + if (box->findData(currentBitrate) == -1) { + int bitrate = FindClosestAvailableAudioBitrate(box, currentBitrate.toInt()); + box->setCurrentText(QString::number(bitrate)); + } else + box->setCurrentText(currentBitrate); + } +} + +static std::tuple aspect_ratio(int cx, int cy) +{ + int common = std::gcd(cx, cy); + int newCX = cx / common; + int newCY = cy / common; + + if (newCX == 8 && newCY == 5) { + newCX = 16; + newCY = 10; + } + + return std::make_tuple(newCX, newCY); +} + +static inline void HighlightGroupBoxLabel(QGroupBox *gb, QWidget *widget, QString objectName) +{ + QFormLayout *layout = qobject_cast(gb->layout()); + + if (!layout) + return; + + QLabel *label = qobject_cast(layout->labelForField(widget)); + + if (label) { + label->setObjectName(objectName); + + label->style()->unpolish(label); + label->style()->polish(label); + } +} + +void RestrictResetBitrates(initializer_list boxes, int maxbitrate); + +/* clang-format off */ +#define COMBO_CHANGED &QComboBox::currentIndexChanged +#define EDIT_CHANGED &QLineEdit::textChanged +#define CBEDIT_CHANGED &QComboBox::editTextChanged +#define CHECK_CHANGED &QCheckBox::toggled +#define GROUP_CHANGED &QGroupBox::toggled +#define SCROLL_CHANGED &QSpinBox::valueChanged +#define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged +#define TEXT_CHANGED &QPlainTextEdit::textChanged + +#define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged +#define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed +#define OUTPUTS_CHANGED &OBSBasicSettings::OutputsChanged +#define AUDIO_RESTART &OBSBasicSettings::AudioChangedRestart +#define AUDIO_CHANGED &OBSBasicSettings::AudioChanged +#define VIDEO_RES &OBSBasicSettings::VideoChangedResolution +#define VIDEO_CHANGED &OBSBasicSettings::VideoChanged +#define A11Y_CHANGED &OBSBasicSettings::A11yChanged +#define APPEAR_CHANGED &OBSBasicSettings::AppearanceChanged +#define ADV_CHANGED &OBSBasicSettings::AdvancedChanged +#define ADV_RESTART &OBSBasicSettings::AdvancedChangedRestart +/* clang-format on */ + +OBSBasicSettings::OBSBasicSettings(QWidget *parent) + : QDialog(parent), + main(qobject_cast(parent)), + ui(new Ui::OBSBasicSettings) +{ + string path; + + EnableThreadedMessageBoxes(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + main->EnableOutputs(false); + + ui->listWidget->setAttribute(Qt::WA_MacShowFocusRect, false); + + /* clang-format off */ + HookWidget(ui->language, COMBO_CHANGED, GENERAL_CHANGED); + HookWidget(ui->updateChannelBox, COMBO_CHANGED, GENERAL_CHANGED); + HookWidget(ui->enableAutoUpdates, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->openStatsOnStartup, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->hideOBSFromCapture, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->warnBeforeStreamStart,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->warnBeforeStreamStop, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->warnBeforeRecordStop, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->hideProjectorCursor, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->projectorAlwaysOnTop, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->recordWhenStreaming, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->keepRecordStreamStops,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->replayWhileStreaming, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->keepReplayStreamStops,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->systemTrayEnabled, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->systemTrayWhenStarted,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->systemTrayAlways, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->saveProjectors, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->closeProjectors, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->snappingEnabled, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->screenSnapping, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->centerSnapping, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->sourceSnapping, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->snapDistance, DSCROLL_CHANGED,GENERAL_CHANGED); + HookWidget(ui->overflowHide, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->overflowAlwaysVisible,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->overflowSelectionHide,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->previewSafeAreas, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->automaticSearch, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->previewSpacingHelpers,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->doubleClickSwitch, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->studioPortraitLayout, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->prevProgLabelToggle, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewMouseSwitch, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewDrawNames, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewDrawAreas, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); + HookWidget(ui->theme, COMBO_CHANGED, APPEAR_CHANGED); + HookWidget(ui->themeVariant, COMBO_CHANGED, APPEAR_CHANGED); + HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); + HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); + HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->serviceCustomServer, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED); + HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumVideoTracksAuto, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumVideoTracks, SCROLL_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoStreamDumpEnable, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoConfigOverrideEnable, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoConfigOverride, TEXT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutputVBitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutStrEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutStrAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutputABitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutAdvanced, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutPreset, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutCustom, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecQuality, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleReplayBuf, GROUP_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRescaleFilter, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMultiTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecType, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecPath, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecRescaleFilter, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFile, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileType, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileTime, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileSize, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->flvTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFType, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFRecPath, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFURL, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFFormat, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFMCfg, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFVBitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFVGOPSize, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFUseRescale, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFIgnoreCompat, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFVEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFVCfg, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFABitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutFFACfg, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack1Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack1Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack2Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack2Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack3Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack3Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack4Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack4Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack5Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack5Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack6Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutTrack6Name, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advReplayBuf, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->channelSetup, COMBO_CHANGED, AUDIO_RESTART); + HookWidget(ui->sampleRate, COMBO_CHANGED, AUDIO_RESTART); + HookWidget(ui->meterDecayRate, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->peakMeterType, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->desktopAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->desktopAudioDevice2, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->auxAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->auxAudioDevice2, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->auxAudioDevice3, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->auxAudioDevice4, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->baseResolution, CBEDIT_CHANGED, VIDEO_RES); + HookWidget(ui->outputResolution, CBEDIT_CHANGED, VIDEO_RES); + HookWidget(ui->downscaleFilter, COMBO_CHANGED, VIDEO_CHANGED); + HookWidget(ui->fpsType, COMBO_CHANGED, VIDEO_CHANGED); + HookWidget(ui->fpsCommon, COMBO_CHANGED, VIDEO_CHANGED); + HookWidget(ui->fpsInteger, SCROLL_CHANGED, VIDEO_CHANGED); + HookWidget(ui->fpsNumerator, SCROLL_CHANGED, VIDEO_CHANGED); + HookWidget(ui->fpsDenominator, SCROLL_CHANGED, VIDEO_CHANGED); + HookWidget(ui->colorsGroupBox, GROUP_CHANGED, A11Y_CHANGED); + HookWidget(ui->colorPreset, COMBO_CHANGED, A11Y_CHANGED); + HookWidget(ui->renderer, COMBO_CHANGED, ADV_RESTART); + HookWidget(ui->adapter, COMBO_CHANGED, ADV_RESTART); + HookWidget(ui->colorFormat, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->colorSpace, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->sdrWhiteLevel, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->hdrNominalPeakLevel, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED); + if (obs_audio_monitoring_available()) + HookWidget(ui->monitoringDevice, COMBO_CHANGED, ADV_CHANGED); +#ifdef _WIN32 + HookWidget(ui->disableAudioDucking, CHECK_CHANGED, ADV_CHANGED); +#endif +#if defined(_WIN32) || defined(__APPLE__) + HookWidget(ui->browserHWAccel, CHECK_CHANGED, ADV_RESTART); +#endif + HookWidget(ui->filenameFormatting, EDIT_CHANGED, ADV_CHANGED); + HookWidget(ui->overwriteIfExists, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->simpleRBPrefix, EDIT_CHANGED, ADV_CHANGED); + HookWidget(ui->simpleRBSuffix, EDIT_CHANGED, ADV_CHANGED); + HookWidget(ui->streamDelayEnable, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->streamDelaySec, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->streamDelayPreserve, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->reconnectEnable, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->reconnectRetryDelay, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->reconnectMaxRetries, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->processPriority, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->confirmOnExit, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->bindToIP, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->ipFamily, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->enableNewSocketLoop, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->enableLowLatencyMode, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->hotkeyFocusType, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->autoRemux, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->dynBitrate, CHECK_CHANGED, ADV_CHANGED); + /* clang-format on */ + +#define ADD_HOTKEY_FOCUS_TYPE(s) ui->hotkeyFocusType->addItem(QTStr("Basic.Settings.Advanced.Hotkeys." s), s) + + ADD_HOTKEY_FOCUS_TYPE("NeverDisableHotkeys"); + ADD_HOTKEY_FOCUS_TYPE("DisableHotkeysInFocus"); + ADD_HOTKEY_FOCUS_TYPE("DisableHotkeysOutOfFocus"); + +#undef ADD_HOTKEY_FOCUS_TYPE + + ui->simpleOutputVBitrate->setSingleStep(50); + ui->simpleOutputVBitrate->setSuffix(" Kbps"); + ui->advOutFFVBitrate->setSingleStep(50); + ui->advOutFFVBitrate->setSuffix(" Kbps"); + ui->advOutFFABitrate->setSuffix(" Kbps"); + +#if !defined(_WIN32) && !defined(ENABLE_SPARKLE_UPDATER) + delete ui->updateSettingsGroupBox; + ui->updateSettingsGroupBox = nullptr; + ui->updateChannelLabel = nullptr; + ui->updateChannelBox = nullptr; + ui->enableAutoUpdates = nullptr; +#else + // Hide update section if disabled + if (App()->IsUpdaterDisabled()) + ui->updateSettingsGroupBox->hide(); +#endif + + // Remove the Advanced Audio section if monitoring is not supported, as the monitoring device selection is the only item in the group box. + if (!obs_audio_monitoring_available()) { + delete ui->monitoringDeviceLabel; + ui->monitoringDeviceLabel = nullptr; + delete ui->monitoringDevice; + ui->monitoringDevice = nullptr; + } + +#ifdef _WIN32 + if (!SetDisplayAffinitySupported()) { + delete ui->hideOBSFromCapture; + ui->hideOBSFromCapture = nullptr; + } + + static struct ProcessPriority { + const char *name; + const char *val; + } processPriorities[] = { + {"Basic.Settings.Advanced.General.ProcessPriority.High", "High"}, + {"Basic.Settings.Advanced.General.ProcessPriority.AboveNormal", "AboveNormal"}, + {"Basic.Settings.Advanced.General.ProcessPriority.Normal", "Normal"}, + {"Basic.Settings.Advanced.General.ProcessPriority.BelowNormal", "BelowNormal"}, + {"Basic.Settings.Advanced.General.ProcessPriority.Idle", "Idle"}, + }; + + for (ProcessPriority pri : processPriorities) + ui->processPriority->addItem(QTStr(pri.name), pri.val); + +#else + delete ui->rendererLabel; + delete ui->renderer; + delete ui->adapterLabel; + delete ui->adapter; + delete ui->processPriorityLabel; + delete ui->processPriority; + delete ui->enableNewSocketLoop; + delete ui->enableLowLatencyMode; + delete ui->hideOBSFromCapture; +#ifdef __linux__ + delete ui->browserHWAccel; + delete ui->sourcesGroup; +#endif + delete ui->disableAudioDucking; + + ui->rendererLabel = nullptr; + ui->renderer = nullptr; + ui->adapterLabel = nullptr; + ui->adapter = nullptr; + ui->processPriorityLabel = nullptr; + ui->processPriority = nullptr; + ui->enableNewSocketLoop = nullptr; + ui->enableLowLatencyMode = nullptr; + ui->hideOBSFromCapture = nullptr; +#ifdef __linux__ + ui->browserHWAccel = nullptr; + ui->sourcesGroup = nullptr; +#endif + ui->disableAudioDucking = nullptr; +#endif + +#ifndef __APPLE__ + delete ui->disableOSXVSync; + delete ui->resetOSXVSync; + ui->disableOSXVSync = nullptr; + ui->resetOSXVSync = nullptr; +#endif + + connect(ui->streamDelaySec, &QSpinBox::valueChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->outputMode, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack1Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack2Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack3Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack4Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack5Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(ui->advOutTrack6Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::UpdateStreamDelayEstimate); + + //Apply button disabled until change. + EnableApplyButton(false); + + installEventFilter(new SettingsEventFilter()); + + LoadColorRanges(); + LoadColorSpaces(); + LoadColorFormats(); + LoadFormats(); + + auto ReloadAudioSources = [](void *data, calldata_t *param) { + auto settings = static_cast(data); + auto source = static_cast(calldata_ptr(param, "source")); + + if (!source) + return; + + if (!(obs_source_get_output_flags(source) & OBS_SOURCE_AUDIO)) + return; + + QMetaObject::invokeMethod(settings, "ReloadAudioSources", Qt::QueuedConnection); + }; + sourceCreated.Connect(obs_get_signal_handler(), "source_create", ReloadAudioSources, this); + channelChanged.Connect(obs_get_signal_handler(), "channel_change", ReloadAudioSources, this); + + hotkeyConflictIcon = QIcon::fromTheme("obs", QIcon(":/res/images/warning.svg")); + + auto ReloadHotkeys = [](void *data, calldata_t *) { + auto settings = static_cast(data); + QMetaObject::invokeMethod(settings, "ReloadHotkeys"); + }; + hotkeyRegistered.Connect(obs_get_signal_handler(), "hotkey_register", ReloadHotkeys, this); + + auto ReloadHotkeysIgnore = [](void *data, calldata_t *param) { + auto settings = static_cast(data); + auto key = static_cast(calldata_ptr(param, "key")); + QMetaObject::invokeMethod(settings, "ReloadHotkeys", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); + }; + hotkeyUnregistered.Connect(obs_get_signal_handler(), "hotkey_unregister", ReloadHotkeysIgnore, this); + + FillSimpleRecordingValues(); + if (obs_audio_monitoring_available()) + FillAudioMonitoringDevices(); + + connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SurroundWarning); + connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SpeakerLayoutChanged); + connect(ui->lowLatencyBuffering, &QCheckBox::clicked, this, &OBSBasicSettings::LowLatencyBufferingChanged); + connect(ui->simpleOutRecQuality, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingQualityChanged); + connect(ui->simpleOutRecQuality, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingQualityLosslessWarning); + connect(ui->simpleOutRecFormat, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutStrEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleStreamingEncoderChanged); + connect(ui->simpleOutStrEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutRecEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutRecAEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleOutAdvanced, &QCheckBox::toggled, this, &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->ignoreRecommended, &QCheckBox::toggled, this, &OBSBasicSettings::SimpleRecordingEncoderChanged); + connect(ui->simpleReplayBuf, &QGroupBox::toggled, this, &OBSBasicSettings::SimpleReplayBufferChanged); + connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, &OBSBasicSettings::SimpleReplayBufferChanged); + connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleReplayBufferChanged); + connect(ui->simpleRBSecMax, &QSpinBox::valueChanged, this, &OBSBasicSettings::SimpleReplayBufferChanged); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + connect(ui->advOutSplitFile, &QCheckBox::checkStateChanged, this, &OBSBasicSettings::AdvOutSplitFileChanged); +#else + connect(ui->advOutSplitFile, &QCheckBox::stateChanged, this, &OBSBasicSettings::AdvOutSplitFileChanged); +#endif + connect(ui->advOutSplitFileType, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvOutSplitFileChanged); + connect(ui->advReplayBuf, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack1, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack2, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack3, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack4, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack5, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecTrack6, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack1Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack2Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack3Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack4Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack5Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutTrack6Bitrate, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecType, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advOutRecEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advRBSecMax, &QSpinBox::valueChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); + + // GPU scaling filters + auto addScaleFilter = [&](const char *string, int value) -> void { + ui->advOutRescaleFilter->addItem(QTStr(string), value); + ui->advOutRecRescaleFilter->addItem(QTStr(string), value); + }; + + addScaleFilter("Basic.Settings.Output.Adv.Rescale.Disabled", OBS_SCALE_DISABLE); + addScaleFilter("Basic.Settings.Video.DownscaleFilter.Bilinear", OBS_SCALE_BILINEAR); + addScaleFilter("Basic.Settings.Video.DownscaleFilter.Area", OBS_SCALE_AREA); + addScaleFilter("Basic.Settings.Video.DownscaleFilter.Bicubic", OBS_SCALE_BICUBIC); + addScaleFilter("Basic.Settings.Video.DownscaleFilter.Lanczos", OBS_SCALE_LANCZOS); + + auto connectScaleFilter = [&](QComboBox *filter, QComboBox *res) -> void { + connect(filter, &QComboBox::currentIndexChanged, this, + [this, res, filter](int) { res->setEnabled(filter->currentData() != OBS_SCALE_DISABLE); }); + }; + + connectScaleFilter(ui->advOutRescaleFilter, ui->advOutRescale); + connectScaleFilter(ui->advOutRecRescaleFilter, ui->advOutRecRescale); + + // Get Bind to IP Addresses + obs_properties_t *ppts = obs_get_output_properties("rtmp_output"); + obs_property_t *p = obs_properties_get(ppts, "bind_ip"); + + size_t count = obs_property_list_item_count(p); + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + const char *val = obs_property_list_item_string(p, i); + + ui->bindToIP->addItem(QT_UTF8(name), val); + } + + // Add IP Family options + p = obs_properties_get(ppts, "ip_family"); + + count = obs_property_list_item_count(p); + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + const char *val = obs_property_list_item_string(p, i); + + ui->ipFamily->addItem(QT_UTF8(name), val); + } + + obs_properties_destroy(ppts); + + ui->multitrackVideoNoticeBox->setVisible(false); + + InitStreamPage(); + InitAppearancePage(); + LoadSettings(false); + + ui->advOutTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->advOutRecTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutRecTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutRecTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutRecTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutRecTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutRecTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->advOutFFTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutFFTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutFFTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutFFTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutFFTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutFFTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->snappingEnabled->setAccessibleName(QTStr("Basic.Settings.General.Snapping")); + ui->systemTrayEnabled->setAccessibleName(QTStr("Basic.Settings.General.SysTray")); + ui->label_31->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Recording.RecType")); + ui->streamDelayEnable->setAccessibleName(QTStr("Basic.Settings.Advanced.StreamDelay")); + ui->reconnectEnable->setAccessibleName(QTStr("Basic.Settings.Output.Reconnect")); + + // Add warning checks to advanced output recording section controls + connect(ui->advOutRecTrack1, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecTrack2, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecTrack3, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecTrack4, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecTrack5, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecTrack6, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecFormat, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + connect(ui->advOutRecEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckWarnings); + + // Check codec compatibility when format (container) changes + connect(ui->advOutRecFormat, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckCodecs); + + // Set placeholder used when selection was reset due to incompatibilities + ui->advOutAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); + ui->advOutRecEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); + ui->advOutRecAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); + ui->simpleOutRecEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); + ui->simpleOutRecAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); + ui->simpleOutRecFormat->setPlaceholderText(QTStr("CodecCompat.ContainerPlaceholder")); + + SimpleRecordingQualityChanged(); + AdvOutSplitFileChanged(); + AdvOutRecCheckCodecs(); + AdvOutRecCheckWarnings(); + + UpdateAutomaticReplayBufferCheckboxes(); + + App()->DisableHotkeys(); + + channelIndex = ui->channelSetup->currentIndex(); + sampleRateIndex = ui->sampleRate->currentIndex(); + llBufferingEnabled = ui->lowLatencyBuffering->isChecked(); + + QRegularExpression rx("\\d{1,5}x\\d{1,5}"); + QValidator *validator = new QRegularExpressionValidator(rx, this); + ui->baseResolution->lineEdit()->setValidator(validator); + ui->outputResolution->lineEdit()->setValidator(validator); + ui->advOutRescale->lineEdit()->setValidator(validator); + ui->advOutRecRescale->lineEdit()->setValidator(validator); + ui->advOutFFRescale->lineEdit()->setValidator(validator); + + connect(ui->useStreamKeyAdv, &QCheckBox::clicked, this, &OBSBasicSettings::UseStreamKeyAdvClicked); + + connect(ui->simpleOutStrAEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::SimpleStreamAudioEncoderChanged); + connect(ui->advOutAEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvAudioEncodersChanged); + connect(ui->advOutRecAEncoder, &QComboBox::currentIndexChanged, this, + &OBSBasicSettings::AdvAudioEncodersChanged); + + UpdateAudioWarnings(); + UpdateAdvNetworkGroup(); + + ui->audioMsg->setVisible(false); + ui->advancedMsg->setVisible(false); + ui->advancedMsg2->setVisible(false); +} + +OBSBasicSettings::~OBSBasicSettings() +{ + delete ui->filenameFormatting->completer(); + main->EnableOutputs(true); + + App()->UpdateHotkeyFocusSetting(); + + EnableThreadedMessageBoxes(false); +} + +void OBSBasicSettings::SaveCombo(QComboBox *widget, const char *section, const char *value) +{ + if (WidgetChanged(widget)) + config_set_string(main->Config(), section, value, QT_TO_UTF8(widget->currentText())); +} + +void OBSBasicSettings::SaveComboData(QComboBox *widget, const char *section, const char *value) +{ + if (WidgetChanged(widget)) { + QString str = GetComboData(widget); + config_set_string(main->Config(), section, value, QT_TO_UTF8(str)); + } +} + +void OBSBasicSettings::SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert) +{ + if (WidgetChanged(widget)) { + bool checked = widget->isChecked(); + if (invert) + checked = !checked; + + config_set_bool(main->Config(), section, value, checked); + } +} + +void OBSBasicSettings::SaveEdit(QLineEdit *widget, const char *section, const char *value) +{ + if (WidgetChanged(widget)) + config_set_string(main->Config(), section, value, QT_TO_UTF8(widget->text())); +} + +void OBSBasicSettings::SaveSpinBox(QSpinBox *widget, const char *section, const char *value) +{ + if (WidgetChanged(widget)) + config_set_int(main->Config(), section, value, widget->value()); +} + +void OBSBasicSettings::SaveText(QPlainTextEdit *widget, const char *section, const char *value) +{ + if (!WidgetChanged(widget)) + return; + + auto utf8 = widget->toPlainText().toUtf8(); + + OBSDataAutoRelease safe_text = obs_data_create(); + obs_data_set_string(safe_text, "text", utf8.constData()); + + config_set_string(main->Config(), section, value, obs_data_get_json(safe_text)); +} + +std::string DeserializeConfigText(const char *value) +{ + OBSDataAutoRelease data = obs_data_create_from_json(value); + return obs_data_get_string(data, "text"); +} + +void OBSBasicSettings::SaveGroupBox(QGroupBox *widget, const char *section, const char *value) +{ + if (WidgetChanged(widget)) + config_set_bool(main->Config(), section, value, widget->isChecked()); +} + +#define CS_PARTIAL_STR QTStr("Basic.Settings.Advanced.Video.ColorRange.Partial") +#define CS_FULL_STR QTStr("Basic.Settings.Advanced.Video.ColorRange.Full") + +void OBSBasicSettings::LoadColorRanges() +{ + ui->colorRange->addItem(CS_PARTIAL_STR, "Partial"); + ui->colorRange->addItem(CS_FULL_STR, "Full"); +} + +#define CS_SRGB_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.sRGB") +#define CS_709_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.709") +#define CS_601_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.601") +#define CS_2100PQ_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.2100PQ") +#define CS_2100HLG_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.2100HLG") + +void OBSBasicSettings::LoadColorSpaces() +{ + ui->colorSpace->addItem(CS_SRGB_STR, "sRGB"); + ui->colorSpace->addItem(CS_709_STR, "709"); + ui->colorSpace->addItem(CS_601_STR, "601"); + ui->colorSpace->addItem(CS_2100PQ_STR, "2100PQ"); + ui->colorSpace->addItem(CS_2100HLG_STR, "2100HLG"); +} + +#define CF_NV12_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.NV12") +#define CF_I420_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I420") +#define CF_I444_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I444") +#define CF_P010_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P010") +#define CF_I010_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I010") +#define CF_P216_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P216") +#define CF_P416_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P416") +#define CF_BGRA_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.BGRA") + +void OBSBasicSettings::LoadColorFormats() +{ + ui->colorFormat->addItem(CF_NV12_STR, "NV12"); + ui->colorFormat->addItem(CF_I420_STR, "I420"); + ui->colorFormat->addItem(CF_I444_STR, "I444"); + ui->colorFormat->addItem(CF_P010_STR, "P010"); + ui->colorFormat->addItem(CF_I010_STR, "I010"); + ui->colorFormat->addItem(CF_P216_STR, "P216"); + ui->colorFormat->addItem(CF_P416_STR, "P416"); + ui->colorFormat->addItem(CF_BGRA_STR, "RGB"); // Avoid config break +} + +#define AV_FORMAT_DEFAULT_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatDefault") +#define AUDIO_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatAudio") +#define VIDEO_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatVideo") + +void OBSBasicSettings::LoadFormats() +{ +#define FORMAT_STR(str) QTStr("Basic.Settings.Output.Format." str) + ui->advOutFFFormat->blockSignals(true); + + formats = GetSupportedFormats(); + + for (auto &format : formats) { + bool audio = format.HasAudio(); + bool video = format.HasVideo(); + + if (audio || video) { + QString itemText(format.name); + if (audio ^ video) + itemText += QString(" (%1)").arg(audio ? AUDIO_STR : VIDEO_STR); + + ui->advOutFFFormat->addItem(itemText, QVariant::fromValue(format)); + } + } + + ui->advOutFFFormat->model()->sort(0); + + ui->advOutFFFormat->insertItem(0, AV_FORMAT_DEFAULT_STR); + + ui->advOutFFFormat->blockSignals(false); + + ui->simpleOutRecFormat->addItem(FORMAT_STR("FLV"), "flv"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("MOV"), "mov"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov"); + ui->simpleOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts"); + + ui->advOutRecFormat->addItem(FORMAT_STR("FLV"), "flv"); + ui->advOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv"); + ui->advOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4"); + ui->advOutRecFormat->addItem(FORMAT_STR("MOV"), "mov"); + ui->advOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4"); + ui->advOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4"); + ui->advOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov"); + ui->advOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts"); + ui->advOutRecFormat->addItem(FORMAT_STR("HLS"), "hls"); + +#undef FORMAT_STR +} + +static void AddCodec(QComboBox *combo, const FFmpegCodec &codec) +{ + QString itemText; + if (codec.long_name) + itemText = QString("%1 - %2").arg(codec.name, codec.long_name); + else + itemText = codec.name; + + combo->addItem(itemText, QVariant::fromValue(codec)); +} + +#define AV_ENCODER_DEFAULT_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.AVEncoderDefault") + +static void AddDefaultCodec(QComboBox *combo, const FFmpegFormat &format, FFmpegCodecType codecType) +{ + FFmpegCodec codec = format.GetDefaultEncoder(codecType); + + int existingIdx = FindEncoder(combo, codec.name, codec.id); + if (existingIdx >= 0) + combo->removeItem(existingIdx); + + QString itemText; + if (codec.long_name) { + itemText = QString("%1 - %2 (%3)").arg(codec.name, codec.long_name, AV_ENCODER_DEFAULT_STR); + } else { + itemText = QString("%1 (%2)").arg(codec.name, AV_ENCODER_DEFAULT_STR); + } + + combo->addItem(itemText, QVariant::fromValue(codec)); +} + +#define AV_ENCODER_DISABLE_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.AVEncoderDisable") + +void OBSBasicSettings::ReloadCodecs(const FFmpegFormat &format) +{ + ui->advOutFFAEncoder->blockSignals(true); + ui->advOutFFVEncoder->blockSignals(true); + ui->advOutFFAEncoder->clear(); + ui->advOutFFVEncoder->clear(); + + bool ignore_compatibility = ui->advOutFFIgnoreCompat->isChecked(); + vector supportedCodecs = GetFormatCodecs(format, ignore_compatibility); + + for (auto &codec : supportedCodecs) { + switch (codec.type) { + case FFmpegCodecType::AUDIO: + AddCodec(ui->advOutFFAEncoder, codec); + break; + case FFmpegCodecType::VIDEO: + AddCodec(ui->advOutFFVEncoder, codec); + break; + default: + break; + } + } + + if (format.HasAudio()) + AddDefaultCodec(ui->advOutFFAEncoder, format, FFmpegCodecType::AUDIO); + if (format.HasVideo()) + AddDefaultCodec(ui->advOutFFVEncoder, format, FFmpegCodecType::VIDEO); + + ui->advOutFFAEncoder->model()->sort(0); + ui->advOutFFVEncoder->model()->sort(0); + + QVariant disable = QVariant::fromValue(FFmpegCodec()); + + ui->advOutFFAEncoder->insertItem(0, AV_ENCODER_DISABLE_STR, disable); + ui->advOutFFVEncoder->insertItem(0, AV_ENCODER_DISABLE_STR, disable); + + ui->advOutFFAEncoder->blockSignals(false); + ui->advOutFFVEncoder->blockSignals(false); +} + +void OBSBasicSettings::LoadLanguageList() +{ + const char *currentLang = App()->GetLocale(); + + ui->language->clear(); + + for (const auto &locale : GetLocaleNames()) { + int idx = ui->language->count(); + + ui->language->addItem(QT_UTF8(locale.second.c_str()), QT_UTF8(locale.first.c_str())); + + if (locale.first == currentLang) + ui->language->setCurrentIndex(idx); + } + + ui->language->model()->sort(0); +} + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +void TranslateBranchInfo(const QString &name, QString &displayName, QString &description) +{ + QString translatedName = QTStr("Basic.Settings.General.ChannelName." + name.toUtf8()); + QString translatedDesc = QTStr("Basic.Settings.General.ChannelDescription." + name.toUtf8()); + + if (!translatedName.startsWith("Basic.Settings.")) + displayName = translatedName; + if (!translatedDesc.startsWith("Basic.Settings.")) + description = translatedDesc; +} +#endif + +void OBSBasicSettings::LoadBranchesList() +{ +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + bool configBranchRemoved = true; + QString configBranch = config_get_string(App()->GetAppConfig(), "General", "UpdateBranch"); + + for (const UpdateBranch &branch : App()->GetBranches()) { + if (branch.name == configBranch) + configBranchRemoved = false; + if (!branch.is_visible && branch.name != configBranch) + continue; + + QString displayName = branch.display_name; + QString description = branch.description; + + TranslateBranchInfo(branch.name, displayName, description); + QString itemDesc = displayName + " - " + description; + + if (!branch.is_enabled) { + itemDesc.prepend(" "); + itemDesc.prepend(QTStr("Basic.Settings.General.UpdateChannelDisabled")); + } else if (branch.name == "stable") { + itemDesc.append(" "); + itemDesc.append(QTStr("Basic.Settings.General.UpdateChannelDefault")); + } + + ui->updateChannelBox->addItem(itemDesc, branch.name); + + // Disable item if branch is disabled + if (!branch.is_enabled) { + QStandardItemModel *model = dynamic_cast(ui->updateChannelBox->model()); + QStandardItem *item = model->item(ui->updateChannelBox->count() - 1); + item->setFlags(Qt::NoItemFlags); + } + } + + // Fall back to default if not yet set or user-selected branch has been removed + if (configBranch.isEmpty() || configBranchRemoved) + configBranch = "stable"; + + int idx = ui->updateChannelBox->findData(configBranch); + ui->updateChannelBox->setCurrentIndex(idx); +#endif +} + +void OBSBasicSettings::LoadGeneralSettings() +{ + loading = true; + + LoadLanguageList(); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + bool enableAutoUpdates = config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates"); + ui->enableAutoUpdates->setChecked(enableAutoUpdates); + + LoadBranchesList(); +#endif + bool openStatsOnStartup = config_get_bool(main->Config(), "General", "OpenStatsOnStartup"); + ui->openStatsOnStartup->setChecked(openStatsOnStartup); + +#if defined(_WIN32) + if (ui->hideOBSFromCapture) { + bool hideWindowFromCapture = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + ui->hideOBSFromCapture->setChecked(hideWindowFromCapture); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + connect(ui->hideOBSFromCapture, &QCheckBox::checkStateChanged, this, + &OBSBasicSettings::HideOBSWindowWarning); +#else + connect(ui->hideOBSFromCapture, &QCheckBox::stateChanged, this, + &OBSBasicSettings::HideOBSWindowWarning); +#endif + } +#endif + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + ui->recordWhenStreaming->setChecked(recordWhenStreaming); + + bool keepRecordStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + ui->keepRecordStreamStops->setChecked(keepRecordStreamStops); + + bool replayWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + ui->replayWhileStreaming->setChecked(replayWhileStreaming); + + bool keepReplayStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + ui->keepReplayStreamStops->setChecked(keepReplayStreamStops); + + bool systemTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + ui->systemTrayEnabled->setChecked(systemTrayEnabled); + + bool systemTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + ui->systemTrayWhenStarted->setChecked(systemTrayWhenStarted); + + bool systemTrayAlways = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); + ui->systemTrayAlways->setChecked(systemTrayAlways); + + bool saveProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + ui->saveProjectors->setChecked(saveProjectors); + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + ui->closeProjectors->setChecked(closeProjectors); + + bool snappingEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SnappingEnabled"); + ui->snappingEnabled->setChecked(snappingEnabled); + + bool screenSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ScreenSnapping"); + ui->screenSnapping->setChecked(screenSnapping); + + bool centerSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CenterSnapping"); + ui->centerSnapping->setChecked(centerSnapping); + + bool sourceSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SourceSnapping"); + ui->sourceSnapping->setChecked(sourceSnapping); + + double snapDistance = config_get_double(App()->GetUserConfig(), "BasicWindow", "SnapDistance"); + ui->snapDistance->setValue(snapDistance); + + bool warnBeforeStreamStart = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + ui->warnBeforeStreamStart->setChecked(warnBeforeStreamStart); + + bool spacingHelpersEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); + ui->previewSpacingHelpers->setChecked(spacingHelpersEnabled); + + bool warnBeforeStreamStop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + ui->warnBeforeStreamStop->setChecked(warnBeforeStreamStop); + + bool warnBeforeRecordStop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + ui->warnBeforeRecordStop->setChecked(warnBeforeRecordStop); + + bool hideProjectorCursor = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideProjectorCursor"); + ui->hideProjectorCursor->setChecked(hideProjectorCursor); + + bool projectorAlwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ProjectorAlwaysOnTop"); + ui->projectorAlwaysOnTop->setChecked(projectorAlwaysOnTop); + + bool overflowHide = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + ui->overflowHide->setChecked(overflowHide); + + bool overflowAlwaysVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + ui->overflowAlwaysVisible->setChecked(overflowAlwaysVisible); + + bool overflowSelectionHide = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + ui->overflowSelectionHide->setChecked(overflowSelectionHide); + + bool safeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); + ui->previewSafeAreas->setChecked(safeAreas); + + bool automaticSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + ui->automaticSearch->setChecked(automaticSearch); + + bool doubleClickSwitch = config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + ui->doubleClickSwitch->setChecked(doubleClickSwitch); + + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + ui->studioPortraitLayout->setChecked(studioPortraitLayout); + + bool prevProgLabels = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels"); + ui->prevProgLabelToggle->setChecked(prevProgLabels); + + bool multiviewMouseSwitch = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewMouseSwitch"); + ui->multiviewMouseSwitch->setChecked(multiviewMouseSwitch); + + bool multiviewDrawNames = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawNames"); + ui->multiviewDrawNames->setChecked(multiviewDrawNames); + + bool multiviewDrawAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawAreas"); + ui->multiviewDrawAreas->setChecked(multiviewDrawAreas); + + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Top"), + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Bottom"), + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Vertical.Left"), + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Vertical.Right"), + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.18Scene.Top"), + static_cast(MultiviewLayout::HORIZONTAL_TOP_18_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top"), + static_cast(MultiviewLayout::HORIZONTAL_TOP_24_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.4Scene"), + static_cast(MultiviewLayout::SCENES_ONLY_4_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.9Scene"), + static_cast(MultiviewLayout::SCENES_ONLY_9_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.16Scene"), + static_cast(MultiviewLayout::SCENES_ONLY_16_SCENES)); + ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.25Scene"), + static_cast(MultiviewLayout::SCENES_ONLY_25_SCENES)); + + ui->multiviewLayout->setCurrentIndex(ui->multiviewLayout->findData( + QVariant::fromValue(config_get_int(App()->GetUserConfig(), "BasicWindow", "MultiviewLayout")))); + + prevLangIndex = ui->language->currentIndex(); + + if (obs_video_active()) + ui->language->setEnabled(false); + + loading = false; +} + +void OBSBasicSettings::LoadRendererList() +{ +#ifdef _WIN32 + const char *renderer = config_get_string(App()->GetAppConfig(), "Video", "Renderer"); + + ui->renderer->addItem(QT_UTF8("Direct3D 11")); + if (opt_allow_opengl || strcmp(renderer, "OpenGL") == 0) + ui->renderer->addItem(QT_UTF8("OpenGL")); + + int idx = ui->renderer->findText(QT_UTF8(renderer)); + if (idx == -1) + idx = 0; + + // the video adapter selection is not currently implemented, hide for now + // to avoid user confusion. was previously protected by + // if (strcmp(renderer, "OpenGL") == 0) + delete ui->adapter; + delete ui->adapterLabel; + ui->adapter = nullptr; + ui->adapterLabel = nullptr; + + ui->renderer->setCurrentIndex(idx); +#endif +} + +static string ResString(uint32_t cx, uint32_t cy) +{ + stringstream res; + res << cx << "x" << cy; + return res.str(); +} + +/* some nice default output resolution vals */ +static const double vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0}; + +static const size_t numVals = sizeof(vals) / sizeof(double); + +void OBSBasicSettings::ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals) +{ + QString advRescale; + QString advRecRescale; + QString advFFRescale; + QString oldOutputRes; + string bestScale; + int bestPixelDiff = 0x7FFFFFFF; + uint32_t out_cx = outputCX; + uint32_t out_cy = outputCY; + + advRescale = ui->advOutRescale->lineEdit()->text(); + advRecRescale = ui->advOutRecRescale->lineEdit()->text(); + advFFRescale = ui->advOutFFRescale->lineEdit()->text(); + + bool lockedOutputRes = !ui->outputResolution->isEditable(); + + if (!lockedOutputRes) { + ui->outputResolution->blockSignals(true); + ui->outputResolution->clear(); + } + if (ignoreAllSignals) { + ui->advOutRescale->blockSignals(true); + ui->advOutRecRescale->blockSignals(true); + ui->advOutFFRescale->blockSignals(true); + } + ui->advOutRescale->clear(); + ui->advOutRecRescale->clear(); + ui->advOutFFRescale->clear(); + + if (!out_cx || !out_cy) { + out_cx = cx; + out_cy = cy; + oldOutputRes = ui->baseResolution->lineEdit()->text(); + } else { + oldOutputRes = QString::number(out_cx) + "x" + QString::number(out_cy); + } + + for (size_t idx = 0; idx < numVals; idx++) { + uint32_t downscaleCX = uint32_t(double(cx) / vals[idx]); + uint32_t downscaleCY = uint32_t(double(cy) / vals[idx]); + uint32_t outDownscaleCX = uint32_t(double(out_cx) / vals[idx]); + uint32_t outDownscaleCY = uint32_t(double(out_cy) / vals[idx]); + + downscaleCX &= 0xFFFFFFFC; + downscaleCY &= 0xFFFFFFFE; + outDownscaleCX &= 0xFFFFFFFE; + outDownscaleCY &= 0xFFFFFFFE; + + string res = ResString(downscaleCX, downscaleCY); + string outRes = ResString(outDownscaleCX, outDownscaleCY); + if (!lockedOutputRes) + ui->outputResolution->addItem(res.c_str()); + ui->advOutRescale->addItem(outRes.c_str()); + ui->advOutRecRescale->addItem(outRes.c_str()); + ui->advOutFFRescale->addItem(outRes.c_str()); + + /* always try to find the closest output resolution to the + * previously set output resolution */ + int newPixelCount = int(downscaleCX * downscaleCY); + int oldPixelCount = int(out_cx * out_cy); + int diff = abs(newPixelCount - oldPixelCount); + + if (diff < bestPixelDiff) { + bestScale = res; + bestPixelDiff = diff; + } + } + + string res = ResString(cx, cy); + + if (!lockedOutputRes) { + float baseAspect = float(cx) / float(cy); + float outputAspect = float(out_cx) / float(out_cy); + bool closeAspect = close_float(baseAspect, outputAspect, 0.01f); + + if (closeAspect) { + ui->outputResolution->lineEdit()->setText(oldOutputRes); + on_outputResolution_editTextChanged(oldOutputRes); + } else { + ui->outputResolution->lineEdit()->setText(bestScale.c_str()); + on_outputResolution_editTextChanged(bestScale.c_str()); + } + + ui->outputResolution->blockSignals(false); + + if (!closeAspect) { + ui->outputResolution->setProperty("changed", QVariant(true)); + videoChanged = true; + } + } + + if (advRescale.isEmpty()) + advRescale = res.c_str(); + if (advRecRescale.isEmpty()) + advRecRescale = res.c_str(); + if (advFFRescale.isEmpty()) + advFFRescale = res.c_str(); + + ui->advOutRescale->lineEdit()->setText(advRescale); + ui->advOutRecRescale->lineEdit()->setText(advRecRescale); + ui->advOutFFRescale->lineEdit()->setText(advFFRescale); + + if (ignoreAllSignals) { + ui->advOutRescale->blockSignals(false); + ui->advOutRecRescale->blockSignals(false); + ui->advOutFFRescale->blockSignals(false); + } +} + +void OBSBasicSettings::LoadDownscaleFilters() +{ + QString downscaleFilter = ui->downscaleFilter->currentData().toString(); + if (downscaleFilter.isEmpty()) + downscaleFilter = config_get_string(main->Config(), "Video", "ScaleType"); + + ui->downscaleFilter->clear(); + if (ui->baseResolution->currentText() == ui->outputResolution->currentText()) { + ui->downscaleFilter->setEnabled(false); + ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Unavailable"), + downscaleFilter); + } else { + ui->downscaleFilter->setEnabled(true); + ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Bilinear"), + QT_UTF8("bilinear")); + ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Area"), QT_UTF8("area")); + ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Bicubic"), QT_UTF8("bicubic")); + ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Lanczos"), QT_UTF8("lanczos")); + + if (downscaleFilter == "bilinear") + ui->downscaleFilter->setCurrentIndex(0); + else if (downscaleFilter == "lanczos") + ui->downscaleFilter->setCurrentIndex(3); + else if (downscaleFilter == "area") + ui->downscaleFilter->setCurrentIndex(1); + else + ui->downscaleFilter->setCurrentIndex(2); + } +} + +void OBSBasicSettings::LoadResolutionLists() +{ + uint32_t cx = config_get_uint(main->Config(), "Video", "BaseCX"); + uint32_t cy = config_get_uint(main->Config(), "Video", "BaseCY"); + uint32_t out_cx = config_get_uint(main->Config(), "Video", "OutputCX"); + uint32_t out_cy = config_get_uint(main->Config(), "Video", "OutputCY"); + + ui->baseResolution->clear(); + + auto addRes = [this](int cx, int cy) { + QString res = ResString(cx, cy).c_str(); + if (ui->baseResolution->findText(res) == -1) + ui->baseResolution->addItem(res); + }; + + for (QScreen *screen : QGuiApplication::screens()) { + QSize as = screen->size(); + uint32_t as_width = as.width(); + uint32_t as_height = as.height(); + + // Calculate physical screen resolution based on the virtual screen resolution + // They might differ if scaling is enabled, e.g. for HiDPI screens + as_width = round(as_width * screen->devicePixelRatio()); + as_height = round(as_height * screen->devicePixelRatio()); + + addRes(as_width, as_height); + } + + addRes(1920, 1080); + addRes(1280, 720); + + string outputResString = ResString(out_cx, out_cy); + + ui->baseResolution->lineEdit()->setText(ResString(cx, cy).c_str()); + + RecalcOutputResPixels(outputResString.c_str()); + ResetDownscales(cx, cy); + + ui->outputResolution->lineEdit()->setText(outputResString.c_str()); + + std::tuple aspect = aspect_ratio(cx, cy); + + ui->baseAspect->setText( + QTStr("AspectRatio").arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); +} + +static inline void LoadFPSCommon(OBSBasic *main, Ui::OBSBasicSettings *ui) +{ + const char *val = config_get_string(main->Config(), "Video", "FPSCommon"); + + int idx = ui->fpsCommon->findText(val); + if (idx == -1) + idx = 4; + ui->fpsCommon->setCurrentIndex(idx); +} + +static inline void LoadFPSInteger(OBSBasic *main, Ui::OBSBasicSettings *ui) +{ + uint32_t val = config_get_uint(main->Config(), "Video", "FPSInt"); + ui->fpsInteger->setValue(val); +} + +static inline void LoadFPSFraction(OBSBasic *main, Ui::OBSBasicSettings *ui) +{ + uint32_t num = config_get_uint(main->Config(), "Video", "FPSNum"); + uint32_t den = config_get_uint(main->Config(), "Video", "FPSDen"); + + ui->fpsNumerator->setValue(num); + ui->fpsDenominator->setValue(den); +} + +void OBSBasicSettings::LoadFPSData() +{ + LoadFPSCommon(main, ui.get()); + LoadFPSInteger(main, ui.get()); + LoadFPSFraction(main, ui.get()); + + uint32_t fpsType = config_get_uint(main->Config(), "Video", "FPSType"); + if (fpsType > 2) + fpsType = 0; + + ui->fpsType->setCurrentIndex(fpsType); + ui->fpsTypes->setCurrentIndex(fpsType); +} + +void OBSBasicSettings::LoadVideoSettings() +{ + loading = true; + + if (obs_video_active()) { + ui->videoPage->setEnabled(false); + ui->videoMsg->setText(QTStr("Basic.Settings.Video.CurrentlyActive")); + } + + LoadResolutionLists(); + LoadFPSData(); + LoadDownscaleFilters(); + + loading = false; +} + +static inline bool IsSurround(const char *speakers) +{ + static const char *surroundLayouts[] = {"2.1", "4.0", "4.1", "5.1", "7.1", nullptr}; + + if (!speakers || !*speakers) + return false; + + const char **curLayout = surroundLayouts; + for (; *curLayout; ++curLayout) { + if (strcmp(*curLayout, speakers) == 0) { + return true; + } + } + + return false; +} + +void OBSBasicSettings::LoadSimpleOutputSettings() +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + const char *streamEnc = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *streamAudioEnc = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int audioBitrate = config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + const char *preset = config_get_string(main->Config(), "SimpleOutput", "Preset"); + const char *qsvPreset = config_get_string(main->Config(), "SimpleOutput", "QSVPreset"); + const char *nvPreset = config_get_string(main->Config(), "SimpleOutput", "NVENCPreset2"); + const char *amdPreset = config_get_string(main->Config(), "SimpleOutput", "AMDPreset"); + const char *amdAV1Preset = config_get_string(main->Config(), "SimpleOutput", "AMDAV1Preset"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *recQual = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *recEnc = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *recAudioEnc = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + const char *muxCustom = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool replayBuf = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + ui->simpleOutRecTrack1->setChecked(tracks & (1 << 0)); + ui->simpleOutRecTrack2->setChecked(tracks & (1 << 1)); + ui->simpleOutRecTrack3->setChecked(tracks & (1 << 2)); + ui->simpleOutRecTrack4->setChecked(tracks & (1 << 3)); + ui->simpleOutRecTrack5->setChecked(tracks & (1 << 4)); + ui->simpleOutRecTrack6->setChecked(tracks & (1 << 5)); + + curPreset = preset; + curQSVPreset = qsvPreset; + curNVENCPreset = nvPreset; + curAMDPreset = amdPreset; + curAMDAV1Preset = amdAV1Preset; + + bool isOpus = strcmp(streamAudioEnc, "opus") == 0; + audioBitrate = isOpus ? FindClosestAvailableSimpleOpusBitrate(audioBitrate) + : FindClosestAvailableSimpleAACBitrate(audioBitrate); + + ui->simpleOutputPath->setText(path); + ui->simpleNoSpace->setChecked(noSpace); + ui->simpleOutputVBitrate->setValue(videoBitrate); + + int idx = ui->simpleOutRecFormat->findData(format); + ui->simpleOutRecFormat->setCurrentIndex(idx); + + PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); + + const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); + + // restrict list of bitrates when multichannel is OFF + if (!IsSurround(speakers)) + RestrictResetBitrates({ui->simpleOutputABitrate}, 320); + + SetComboByName(ui->simpleOutputABitrate, std::to_string(audioBitrate).c_str()); + + ui->simpleOutAdvanced->setChecked(advanced); + ui->simpleOutCustom->setText(custom); + + idx = ui->simpleOutRecQuality->findData(QString(recQual)); + if (idx == -1) + idx = 0; + ui->simpleOutRecQuality->setCurrentIndex(idx); + + idx = ui->simpleOutStrEncoder->findData(QString(streamEnc)); + if (idx == -1) + idx = 0; + ui->simpleOutStrEncoder->setCurrentIndex(idx); + + idx = ui->simpleOutStrAEncoder->findData(QString(streamAudioEnc)); + if (idx == -1) + idx = 0; + ui->simpleOutStrAEncoder->setCurrentIndex(idx); + + idx = ui->simpleOutRecEncoder->findData(QString(recEnc)); + ui->simpleOutRecEncoder->setCurrentIndex(idx); + + idx = ui->simpleOutRecAEncoder->findData(QString(recAudioEnc)); + ui->simpleOutRecAEncoder->setCurrentIndex(idx); + + ui->simpleOutMuxCustom->setText(muxCustom); + + ui->simpleReplayBuf->setChecked(replayBuf); + ui->simpleRBSecMax->setValue(rbTime); + ui->simpleRBMegsMax->setValue(rbSize); + + SimpleStreamingEncoderChanged(); +} + +static inline QString makeFormatToolTip() +{ + static const char *format_list[][2] = { + {"CCYY", "FilenameFormatting.TT.CCYY"}, {"YY", "FilenameFormatting.TT.YY"}, + {"MM", "FilenameFormatting.TT.MM"}, {"DD", "FilenameFormatting.TT.DD"}, + {"hh", "FilenameFormatting.TT.hh"}, {"mm", "FilenameFormatting.TT.mm"}, + {"ss", "FilenameFormatting.TT.ss"}, {"%", "FilenameFormatting.TT.Percent"}, + {"a", "FilenameFormatting.TT.a"}, {"A", "FilenameFormatting.TT.A"}, + {"b", "FilenameFormatting.TT.b"}, {"B", "FilenameFormatting.TT.B"}, + {"d", "FilenameFormatting.TT.d"}, {"H", "FilenameFormatting.TT.H"}, + {"I", "FilenameFormatting.TT.I"}, {"m", "FilenameFormatting.TT.m"}, + {"M", "FilenameFormatting.TT.M"}, {"p", "FilenameFormatting.TT.p"}, + {"s", "FilenameFormatting.TT.s"}, {"S", "FilenameFormatting.TT.S"}, + {"y", "FilenameFormatting.TT.y"}, {"Y", "FilenameFormatting.TT.Y"}, + {"z", "FilenameFormatting.TT.z"}, {"Z", "FilenameFormatting.TT.Z"}, + {"FPS", "FilenameFormatting.TT.FPS"}, {"CRES", "FilenameFormatting.TT.CRES"}, + {"ORES", "FilenameFormatting.TT.ORES"}, {"VF", "FilenameFormatting.TT.VF"}, + }; + + QString html = ""; + + for (auto f : format_list) { + html += ""; + } + + html += "
%"; + html += f[0]; + html += ""; + html += QTStr(f[1]); + html += "
"; + return html; +} + +void OBSBasicSettings::LoadAdvOutputStreamingSettings() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int trackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int audioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + ui->advOutRescale->setEnabled(rescaleFilter != OBS_SCALE_DISABLE); + ui->advOutRescale->setCurrentText(rescaleRes); + + int idx = ui->advOutRescaleFilter->findData(rescaleFilter); + if (idx != -1) + ui->advOutRescaleFilter->setCurrentIndex(idx); + + QStringList specList = QTStr("FilenameFormatting.completer").split(QRegularExpression("\n")); + QCompleter *specCompleter = new QCompleter(specList); + specCompleter->setCaseSensitivity(Qt::CaseSensitive); + specCompleter->setFilterMode(Qt::MatchContains); + ui->filenameFormatting->setCompleter(specCompleter); + ui->filenameFormatting->setToolTip(makeFormatToolTip()); + + switch (trackIndex) { + case 1: + ui->advOutTrack1->setChecked(true); + break; + case 2: + ui->advOutTrack2->setChecked(true); + break; + case 3: + ui->advOutTrack3->setChecked(true); + break; + case 4: + ui->advOutTrack4->setChecked(true); + break; + case 5: + ui->advOutTrack5->setChecked(true); + break; + case 6: + ui->advOutTrack6->setChecked(true); + break; + } + ui->advOutMultiTrack1->setChecked(audioMixes & (1 << 0)); + ui->advOutMultiTrack2->setChecked(audioMixes & (1 << 1)); + ui->advOutMultiTrack3->setChecked(audioMixes & (1 << 2)); + ui->advOutMultiTrack4->setChecked(audioMixes & (1 << 3)); + ui->advOutMultiTrack5->setChecked(audioMixes & (1 << 4)); + ui->advOutMultiTrack6->setChecked(audioMixes & (1 << 5)); + + obs_service_t *service_obj = main->GetService(); + const char *protocol = nullptr; + protocol = obs_service_get_protocol(service_obj); + SwapMultiTrack(protocol); +} + +OBSPropertiesView *OBSBasicSettings::CreateEncoderPropertyView(const char *encoder, const char *path, bool changed) +{ + OBSDataAutoRelease settings = obs_encoder_defaults(encoder); + OBSPropertiesView *view; + + if (path) { + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(path); + + if (!jsonFilePath.empty()) { + obs_data_t *data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + obs_data_apply(settings, data); + obs_data_release(data); + } + } + + view = new OBSPropertiesView(settings.Get(), encoder, (PropertiesReloadCallback)obs_get_encoder_properties, + 170); + view->setFrameShape(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + view->setProperty("changed", QVariant(changed)); + view->setScrolling(false); + QObject::connect(view, &OBSPropertiesView::Changed, this, &OBSBasicSettings::OutputsChanged); + + return view; +} + +void OBSBasicSettings::LoadAdvOutputStreamingEncoderProperties() +{ + const char *type = config_get_string(main->Config(), "AdvOut", "Encoder"); + + delete streamEncoderProps; + streamEncoderProps = CreateEncoderPropertyView(type, "streamEncoder.json"); + streamEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + ui->advOutEncoderLayout->addWidget(streamEncoderProps); + + connect(streamEncoderProps, &OBSPropertiesView::Changed, this, &OBSBasicSettings::UpdateStreamDelayEstimate); + connect(streamEncoderProps, &OBSPropertiesView::Changed, this, &OBSBasicSettings::AdvReplayBufferChanged); + + curAdvStreamEncoder = type; + + if (!SetComboByValue(ui->advOutEncoder, type)) { + uint32_t caps = obs_get_encoder_caps(type); + if ((caps & ENCODER_HIDE_FLAGS) != 0) { + QString encName = QT_UTF8(obs_encoder_get_display_name(type)); + if (caps & OBS_ENCODER_CAP_DEPRECATED) + encName += " (" + QTStr("Deprecated") + ")"; + + ui->advOutEncoder->insertItem(0, encName, QT_UTF8(type)); + SetComboByValue(ui->advOutEncoder, type); + } + } + + UpdateStreamDelayEstimate(); +} + +void OBSBasicSettings::LoadAdvOutputRecordingSettings() +{ + const char *type = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *format = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + bool noSpace = config_get_bool(main->Config(), "AdvOut", "RecFileNameWithoutSpace"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + int tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + int flvTrack = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + bool splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + const char *splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + int splitFileTime = config_get_int(main->Config(), "AdvOut", "RecSplitFileTime"); + int splitFileSize = config_get_int(main->Config(), "AdvOut", "RecSplitFileSize"); + + int typeIndex = (astrcmpi(type, "FFmpeg") == 0) ? 1 : 0; + ui->advOutRecType->setCurrentIndex(typeIndex); + ui->advOutRecPath->setText(path); + ui->advOutNoSpace->setChecked(noSpace); + ui->advOutRecRescale->setCurrentText(rescaleRes); + int idx = ui->advOutRecRescaleFilter->findData(rescaleFilter); + if (idx != -1) + ui->advOutRecRescaleFilter->setCurrentIndex(idx); + ui->advOutMuxCustom->setText(muxCustom); + + idx = ui->advOutRecFormat->findData(format); + ui->advOutRecFormat->setCurrentIndex(idx); + + ui->advOutRecTrack1->setChecked(tracks & (1 << 0)); + ui->advOutRecTrack2->setChecked(tracks & (1 << 1)); + ui->advOutRecTrack3->setChecked(tracks & (1 << 2)); + ui->advOutRecTrack4->setChecked(tracks & (1 << 3)); + ui->advOutRecTrack5->setChecked(tracks & (1 << 4)); + ui->advOutRecTrack6->setChecked(tracks & (1 << 5)); + + if (astrcmpi(splitFileType, "Size") == 0) + idx = 1; + else if (astrcmpi(splitFileType, "Manual") == 0) + idx = 2; + else + idx = 0; + ui->advOutSplitFile->setChecked(splitFile); + ui->advOutSplitFileType->setCurrentIndex(idx); + ui->advOutSplitFileTime->setValue(splitFileTime); + ui->advOutSplitFileSize->setValue(splitFileSize); + + switch (flvTrack) { + case 1: + ui->flvTrack1->setChecked(true); + break; + case 2: + ui->flvTrack2->setChecked(true); + break; + case 3: + ui->flvTrack3->setChecked(true); + break; + case 4: + ui->flvTrack4->setChecked(true); + break; + case 5: + ui->flvTrack5->setChecked(true); + break; + case 6: + ui->flvTrack6->setChecked(true); + break; + default: + ui->flvTrack1->setChecked(true); + break; + } +} + +void OBSBasicSettings::LoadAdvOutputRecordingEncoderProperties() +{ + const char *type = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + + delete recordEncoderProps; + recordEncoderProps = nullptr; + + if (astrcmpi(type, "none") != 0) { + recordEncoderProps = CreateEncoderPropertyView(type, "recordEncoder.json"); + recordEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + ui->advOutRecEncoderProps->layout()->addWidget(recordEncoderProps); + connect(recordEncoderProps, &OBSPropertiesView::Changed, this, + &OBSBasicSettings::AdvReplayBufferChanged); + } + + curAdvRecordEncoder = type; + + if (!SetComboByValue(ui->advOutRecEncoder, type)) { + uint32_t caps = obs_get_encoder_caps(type); + if ((caps & ENCODER_HIDE_FLAGS) != 0) { + QString encName = QT_UTF8(obs_encoder_get_display_name(type)); + if (caps & OBS_ENCODER_CAP_DEPRECATED) + encName += " (" + QTStr("Deprecated") + ")"; + + ui->advOutRecEncoder->insertItem(1, encName, QT_UTF8(type)); + SetComboByValue(ui->advOutRecEncoder, type); + } else { + ui->advOutRecEncoder->setCurrentIndex(-1); + } + } +} + +static void SelectFormat(QComboBox *combo, const char *name, const char *mimeType) +{ + FFmpegFormat format{name, mimeType}; + + for (int i = 0; i < combo->count(); i++) { + QVariant v = combo->itemData(i); + if (!v.isNull()) { + if (format == v.value()) { + combo->setCurrentIndex(i); + return; + } + } + } + + combo->setCurrentIndex(0); +} + +static void SelectEncoder(QComboBox *combo, const char *name, int id) +{ + int idx = FindEncoder(combo, name, id); + if (idx >= 0) + combo->setCurrentIndex(idx); +} + +void OBSBasicSettings::LoadAdvOutputFFmpegSettings() +{ + bool saveFile = config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + const char *path = config_get_string(main->Config(), "AdvOut", "FFFilePath"); + bool noSpace = config_get_bool(main->Config(), "AdvOut", "FFFileNameWithoutSpace"); + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + const char *format = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + int videoBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + bool codecCompat = config_get_bool(main->Config(), "AdvOut", "FFIgnoreCompat"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int audioBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int audioMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + ui->advOutFFType->setCurrentIndex(saveFile ? 0 : 1); + ui->advOutFFRecPath->setText(QT_UTF8(path)); + ui->advOutFFNoSpace->setChecked(noSpace); + ui->advOutFFURL->setText(QT_UTF8(url)); + SelectFormat(ui->advOutFFFormat, format, mimeType); + ui->advOutFFMCfg->setText(muxCustom); + ui->advOutFFVBitrate->setValue(videoBitrate); + ui->advOutFFVGOPSize->setValue(gopSize); + ui->advOutFFUseRescale->setChecked(rescale); + ui->advOutFFIgnoreCompat->setChecked(codecCompat); + ui->advOutFFRescale->setEnabled(rescale); + ui->advOutFFRescale->setCurrentText(rescaleRes); + SelectEncoder(ui->advOutFFVEncoder, vEncoder, vEncoderId); + ui->advOutFFVCfg->setText(vEncCustom); + ui->advOutFFABitrate->setValue(audioBitrate); + SelectEncoder(ui->advOutFFAEncoder, aEncoder, aEncoderId); + ui->advOutFFACfg->setText(aEncCustom); + + ui->advOutFFTrack1->setChecked(audioMixes & (1 << 0)); + ui->advOutFFTrack2->setChecked(audioMixes & (1 << 1)); + ui->advOutFFTrack3->setChecked(audioMixes & (1 << 2)); + ui->advOutFFTrack4->setChecked(audioMixes & (1 << 3)); + ui->advOutFFTrack5->setChecked(audioMixes & (1 << 4)); + ui->advOutFFTrack6->setChecked(audioMixes & (1 << 5)); +} + +void OBSBasicSettings::LoadAdvOutputAudioSettings() +{ + int track1Bitrate = config_get_uint(main->Config(), "AdvOut", "Track1Bitrate"); + int track2Bitrate = config_get_uint(main->Config(), "AdvOut", "Track2Bitrate"); + int track3Bitrate = config_get_uint(main->Config(), "AdvOut", "Track3Bitrate"); + int track4Bitrate = config_get_uint(main->Config(), "AdvOut", "Track4Bitrate"); + int track5Bitrate = config_get_uint(main->Config(), "AdvOut", "Track5Bitrate"); + int track6Bitrate = config_get_uint(main->Config(), "AdvOut", "Track6Bitrate"); + const char *name1 = config_get_string(main->Config(), "AdvOut", "Track1Name"); + const char *name2 = config_get_string(main->Config(), "AdvOut", "Track2Name"); + const char *name3 = config_get_string(main->Config(), "AdvOut", "Track3Name"); + const char *name4 = config_get_string(main->Config(), "AdvOut", "Track4Name"); + const char *name5 = config_get_string(main->Config(), "AdvOut", "Track5Name"); + const char *name6 = config_get_string(main->Config(), "AdvOut", "Track6Name"); + + const char *encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *rec_encoder_id = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, + ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + encoder_id, strcmp(rec_encoder_id, "none") != 0 ? rec_encoder_id : encoder_id); + + track1Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack1Bitrate, track1Bitrate); + track2Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack2Bitrate, track2Bitrate); + track3Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack3Bitrate, track3Bitrate); + track4Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack4Bitrate, track4Bitrate); + track5Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack5Bitrate, track5Bitrate); + track6Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack6Bitrate, track6Bitrate); + + // restrict list of bitrates when multichannel is OFF + const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); + + // restrict list of bitrates when multichannel is OFF + if (!IsSurround(speakers)) { + RestrictResetBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, + ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + 320); + } + + SetComboByName(ui->advOutTrack1Bitrate, std::to_string(track1Bitrate).c_str()); + SetComboByName(ui->advOutTrack2Bitrate, std::to_string(track2Bitrate).c_str()); + SetComboByName(ui->advOutTrack3Bitrate, std::to_string(track3Bitrate).c_str()); + SetComboByName(ui->advOutTrack4Bitrate, std::to_string(track4Bitrate).c_str()); + SetComboByName(ui->advOutTrack5Bitrate, std::to_string(track5Bitrate).c_str()); + SetComboByName(ui->advOutTrack6Bitrate, std::to_string(track6Bitrate).c_str()); + + ui->advOutTrack1Name->setText(name1); + ui->advOutTrack2Name->setText(name2); + ui->advOutTrack3Name->setText(name3); + ui->advOutTrack4Name->setText(name4); + ui->advOutTrack5Name->setText(name5); + ui->advOutTrack6Name->setText(name6); +} + +void OBSBasicSettings::LoadOutputSettings() +{ + loading = true; + + ResetEncoders(); + + const char *mode = config_get_string(main->Config(), "Output", "Mode"); + + int modeIdx = astrcmpi(mode, "Advanced") == 0 ? 1 : 0; + ui->outputMode->setCurrentIndex(modeIdx); + + LoadSimpleOutputSettings(); + LoadAdvOutputStreamingSettings(); + LoadAdvOutputStreamingEncoderProperties(); + + const char *type = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + if (!SetComboByValue(ui->advOutAEncoder, type)) + ui->advOutAEncoder->setCurrentIndex(-1); + + LoadAdvOutputRecordingSettings(); + LoadAdvOutputRecordingEncoderProperties(); + type = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + if (!SetComboByValue(ui->advOutRecAEncoder, type)) + ui->advOutRecAEncoder->setCurrentIndex(-1); + LoadAdvOutputFFmpegSettings(); + LoadAdvOutputAudioSettings(); + + if (obs_video_active()) { + ui->outputMode->setEnabled(false); + ui->outputModeLabel->setEnabled(false); + ui->simpleOutStrEncoderLabel->setEnabled(false); + ui->simpleOutStrEncoder->setEnabled(false); + ui->simpleOutStrAEncoderLabel->setEnabled(false); + ui->simpleOutStrAEncoder->setEnabled(false); + ui->simpleRecordingGroupBox->setEnabled(false); + ui->simpleReplayBuf->setEnabled(false); + ui->advOutTopContainer->setEnabled(false); + ui->advOutRecTopContainer->setEnabled(false); + ui->advOutRecTypeContainer->setEnabled(false); + ui->advOutputAudioTracksTab->setEnabled(false); + ui->advNetworkGroupBox->setEnabled(false); + } + + loading = false; +} + +void OBSBasicSettings::SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncoder) +{ + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + + switch (encoderType) { + case FFmpegCodecType::VIDEO: + ui->advOutFFVBitrate->setEnabled(enabled); + ui->advOutFFVGOPSize->setEnabled(enabled); + ui->advOutFFUseRescale->setEnabled(enabled); + ui->advOutFFRescale->setEnabled(enabled && rescale); + ui->advOutFFVEncoder->setEnabled(enabled || enableEncoder); + ui->advOutFFVCfg->setEnabled(enabled); + break; + case FFmpegCodecType::AUDIO: + ui->advOutFFABitrate->setEnabled(enabled); + ui->advOutFFAEncoder->setEnabled(enabled || enableEncoder); + ui->advOutFFACfg->setEnabled(enabled); + ui->advOutFFTrack1->setEnabled(enabled); + ui->advOutFFTrack2->setEnabled(enabled); + ui->advOutFFTrack3->setEnabled(enabled); + ui->advOutFFTrack4->setEnabled(enabled); + ui->advOutFFTrack5->setEnabled(enabled); + ui->advOutFFTrack6->setEnabled(enabled); + default: + break; + } +} + +static inline void LoadListValue(QComboBox *widget, const char *text, const char *val) +{ + widget->addItem(QT_UTF8(text), QT_UTF8(val)); +} + +void OBSBasicSettings::LoadListValues(QComboBox *widget, obs_property_t *prop, int index) +{ + size_t count = obs_property_list_item_count(prop); + + OBSSourceAutoRelease source = obs_get_output_source(index); + const char *deviceId = nullptr; + OBSDataAutoRelease settings = nullptr; + + if (source) { + settings = obs_source_get_settings(source); + if (settings) + deviceId = obs_data_get_string(settings, "device_id"); + } + + widget->addItem(QTStr("Basic.Settings.Audio.Disabled"), "disabled"); + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(prop, i); + const char *val = obs_property_list_item_string(prop, i); + LoadListValue(widget, name, val); + } + + if (deviceId) { + QVariant var(QT_UTF8(deviceId)); + int idx = widget->findData(var); + if (idx != -1) { + widget->setCurrentIndex(idx); + } else { + widget->insertItem(0, + QTStr("Basic.Settings.Audio." + "UnknownAudioDevice"), + var); + widget->setCurrentIndex(0); + HighlightGroupBoxLabel(ui->audioDevicesGroupBox, widget, "errorLabel"); + } + } +} + +void OBSBasicSettings::LoadAudioDevices() +{ + const char *input_id = App()->InputAudioSource(); + const char *output_id = App()->OutputAudioSource(); + + obs_properties_t *input_props = obs_get_source_properties(input_id); + obs_properties_t *output_props = obs_get_source_properties(output_id); + + if (input_props) { + obs_property_t *inputs = obs_properties_get(input_props, "device_id"); + LoadListValues(ui->auxAudioDevice1, inputs, 3); + LoadListValues(ui->auxAudioDevice2, inputs, 4); + LoadListValues(ui->auxAudioDevice3, inputs, 5); + LoadListValues(ui->auxAudioDevice4, inputs, 6); + obs_properties_destroy(input_props); + } + + if (output_props) { + obs_property_t *outputs = obs_properties_get(output_props, "device_id"); + LoadListValues(ui->desktopAudioDevice1, outputs, 1); + LoadListValues(ui->desktopAudioDevice2, outputs, 2); + obs_properties_destroy(output_props); + } + + if (obs_video_active()) { + ui->sampleRate->setEnabled(false); + ui->channelSetup->setEnabled(false); + } +} + +#define NBSP "\xC2\xA0" + +void OBSBasicSettings::LoadAudioSources() +{ + if (ui->audioSourceLayout->rowCount() > 0) { + QLayoutItem *forDeletion = ui->audioSourceLayout->takeAt(0); + forDeletion->widget()->deleteLater(); + delete forDeletion; + } + auto layout = new QFormLayout(); + layout->setVerticalSpacing(15); + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + + audioSourceSignals.clear(); + audioSources.clear(); + + auto widget = new QWidget(); + widget->setLayout(layout); + ui->audioSourceLayout->addRow(widget); + + const char *enablePtm = Str("Basic.Settings.Audio.EnablePushToMute"); + const char *ptmDelay = Str("Basic.Settings.Audio.PushToMuteDelay"); + const char *enablePtt = Str("Basic.Settings.Audio.EnablePushToTalk"); + const char *pttDelay = Str("Basic.Settings.Audio.PushToTalkDelay"); + auto AddSource = [&](obs_source_t *source) { + if (!(obs_source_get_output_flags(source) & OBS_SOURCE_AUDIO)) + return true; + + auto form = new QFormLayout(); + form->setVerticalSpacing(0); + form->setHorizontalSpacing(5); + form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + + auto ptmCB = new SilentUpdateCheckBox(); + ptmCB->setText(enablePtm); + ptmCB->setChecked(obs_source_push_to_mute_enabled(source)); + form->addRow(ptmCB); + + auto ptmSB = new SilentUpdateSpinBox(); + ptmSB->setSuffix(NBSP "ms"); + ptmSB->setRange(0, INT_MAX); + ptmSB->setValue(obs_source_get_push_to_mute_delay(source)); + form->addRow(ptmDelay, ptmSB); + + auto pttCB = new SilentUpdateCheckBox(); + pttCB->setText(enablePtt); + pttCB->setChecked(obs_source_push_to_talk_enabled(source)); + form->addRow(pttCB); + + auto pttSB = new SilentUpdateSpinBox(); + pttSB->setSuffix(NBSP "ms"); + pttSB->setRange(0, INT_MAX); + pttSB->setValue(obs_source_get_push_to_talk_delay(source)); + form->addRow(pttDelay, pttSB); + + HookWidget(ptmCB, CHECK_CHANGED, AUDIO_CHANGED); + HookWidget(ptmSB, SCROLL_CHANGED, AUDIO_CHANGED); + HookWidget(pttCB, CHECK_CHANGED, AUDIO_CHANGED); + HookWidget(pttSB, SCROLL_CHANGED, AUDIO_CHANGED); + + audioSourceSignals.reserve(audioSourceSignals.size() + 4); + + auto handler = obs_source_get_signal_handler(source); + audioSourceSignals.emplace_back( + handler, "push_to_mute_changed", + [](void *data, calldata_t *param) { + QMetaObject::invokeMethod(static_cast(data), "setCheckedSilently", + Q_ARG(bool, calldata_bool(param, "enabled"))); + }, + ptmCB); + audioSourceSignals.emplace_back( + handler, "push_to_mute_delay", + [](void *data, calldata_t *param) { + QMetaObject::invokeMethod(static_cast(data), "setValueSilently", + Q_ARG(int, calldata_int(param, "delay"))); + }, + ptmSB); + audioSourceSignals.emplace_back( + handler, "push_to_talk_changed", + [](void *data, calldata_t *param) { + QMetaObject::invokeMethod(static_cast(data), "setCheckedSilently", + Q_ARG(bool, calldata_bool(param, "enabled"))); + }, + pttCB); + audioSourceSignals.emplace_back( + handler, "push_to_talk_delay", + [](void *data, calldata_t *param) { + QMetaObject::invokeMethod(static_cast(data), "setValueSilently", + Q_ARG(int, calldata_int(param, "delay"))); + }, + pttSB); + + audioSources.emplace_back(OBSGetWeakRef(source), ptmCB, ptmSB, pttCB, pttSB); + + auto label = new OBSSourceLabel(source); + TruncateLabel(label, label->text()); + label->setMinimumSize(QSize(170, 0)); + label->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter); + connect(label, &OBSSourceLabel::Removed, + [=]() { QMetaObject::invokeMethod(this, "ReloadAudioSources"); }); + connect(label, &OBSSourceLabel::Destroyed, + [=]() { QMetaObject::invokeMethod(this, "ReloadAudioSources"); }); + + layout->addRow(label, form); + return true; + }; + + using AddSource_t = decltype(AddSource); + obs_enum_sources( + [](void *data, obs_source_t *source) { + auto &AddSource = *static_cast(data); + if (!obs_source_removed(source)) + AddSource(source); + return true; + }, + static_cast(&AddSource)); + + if (layout->rowCount() == 0) + ui->audioHotkeysGroupBox->hide(); + else + ui->audioHotkeysGroupBox->show(); +} + +void OBSBasicSettings::LoadAudioSettings() +{ + uint32_t sampleRate = config_get_uint(main->Config(), "Audio", "SampleRate"); + const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); + double meterDecayRate = config_get_double(main->Config(), "Audio", "MeterDecayRate"); + uint32_t peakMeterTypeIdx = config_get_uint(main->Config(), "Audio", "PeakMeterType"); + bool enableLLAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + + loading = true; + + const char *str; + if (sampleRate == 48000) + str = "48 kHz"; + else + str = "44.1 kHz"; + + int sampleRateIdx = ui->sampleRate->findText(str); + if (sampleRateIdx != -1) + ui->sampleRate->setCurrentIndex(sampleRateIdx); + + if (strcmp(speakers, "Mono") == 0) + ui->channelSetup->setCurrentIndex(0); + else if (strcmp(speakers, "2.1") == 0) + ui->channelSetup->setCurrentIndex(2); + else if (strcmp(speakers, "4.0") == 0) + ui->channelSetup->setCurrentIndex(3); + else if (strcmp(speakers, "4.1") == 0) + ui->channelSetup->setCurrentIndex(4); + else if (strcmp(speakers, "5.1") == 0) + ui->channelSetup->setCurrentIndex(5); + else if (strcmp(speakers, "7.1") == 0) + ui->channelSetup->setCurrentIndex(6); + else + ui->channelSetup->setCurrentIndex(1); + + if (meterDecayRate == VOLUME_METER_DECAY_MEDIUM) + ui->meterDecayRate->setCurrentIndex(1); + else if (meterDecayRate == VOLUME_METER_DECAY_SLOW) + ui->meterDecayRate->setCurrentIndex(2); + else + ui->meterDecayRate->setCurrentIndex(0); + + ui->peakMeterType->setCurrentIndex(peakMeterTypeIdx); + ui->lowLatencyBuffering->setChecked(enableLLAudioBuffering); + + LoadAudioDevices(); + LoadAudioSources(); + + loading = false; +} + +void OBSBasicSettings::UpdateColorFormatSpaceWarning() +{ + const QString format = ui->colorFormat->currentData().toString(); + switch (ui->colorSpace->currentIndex()) { + case 3: /* Rec.2100 (PQ) */ + case 4: /* Rec.2100 (HLG) */ + if ((format == "P010") || (format == "P216") || (format == "P416")) { + ui->advancedMsg2->clear(); + ui->advancedMsg2->setVisible(false); + } else if (format == "I010") { + ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning")); + ui->advancedMsg2->setVisible(true); + } else { + ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning2100")); + ui->advancedMsg2->setVisible(true); + } + break; + default: + if (format == "NV12") { + ui->advancedMsg2->clear(); + ui->advancedMsg2->setVisible(false); + } else if ((format == "I010") || (format == "P010") || (format == "P216") || (format == "P416")) { + ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarningPreciseSdr")); + ui->advancedMsg2->setVisible(true); + } else { + ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning")); + ui->advancedMsg2->setVisible(true); + } + } +} + +void OBSBasicSettings::LoadAdvancedSettings() +{ + const char *videoColorFormat = config_get_string(main->Config(), "Video", "ColorFormat"); + const char *videoColorSpace = config_get_string(main->Config(), "Video", "ColorSpace"); + const char *videoColorRange = config_get_string(main->Config(), "Video", "ColorRange"); + uint32_t sdrWhiteLevel = (uint32_t)config_get_uint(main->Config(), "Video", "SdrWhiteLevel"); + uint32_t hdrNominalPeakLevel = (uint32_t)config_get_uint(main->Config(), "Video", "HdrNominalPeakLevel"); + + QString monDevName; + QString monDevId; + if (obs_audio_monitoring_available()) { + monDevName = config_get_string(main->Config(), "Audio", "MonitoringDeviceName"); + monDevId = config_get_string(main->Config(), "Audio", "MonitoringDeviceId"); + } + bool enableDelay = 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"); + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + const char *filename = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + bool replayBuf = config_get_bool(main->Config(), "AdvOut", "RecRB"); + int rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + const char *hotkeyFocusType = config_get_string(App()->GetUserConfig(), "General", "HotkeyFocusType"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + loading = true; + + LoadRendererList(); + + if (obs_audio_monitoring_available() && !SetComboByValue(ui->monitoringDevice, monDevId.toUtf8())) + SetInvalidValue(ui->monitoringDevice, monDevName.toUtf8(), monDevId.toUtf8()); + + ui->confirmOnExit->setChecked(confirmOnExit); + + ui->filenameFormatting->setText(filename); + ui->overwriteIfExists->setChecked(overwriteIfExists); + ui->simpleRBPrefix->setText(rbPrefix); + ui->simpleRBSuffix->setText(rbSuffix); + + ui->advReplayBuf->setChecked(replayBuf); + ui->advRBSecMax->setValue(rbTime); + ui->advRBMegsMax->setValue(rbSize); + + ui->reconnectEnable->setChecked(reconnect); + ui->reconnectRetryDelay->setValue(retryDelay); + ui->reconnectMaxRetries->setValue(maxRetries); + + ui->streamDelaySec->setValue(delaySec); + ui->streamDelayPreserve->setChecked(preserveDelay); + ui->streamDelayEnable->setChecked(enableDelay); + ui->autoRemux->setChecked(autoRemux); + ui->dynBitrate->setChecked(dynBitrate); + + SetComboByValue(ui->colorFormat, videoColorFormat); + SetComboByValue(ui->colorSpace, videoColorSpace); + SetComboByValue(ui->colorRange, videoColorRange); + ui->sdrWhiteLevel->setValue(sdrWhiteLevel); + ui->hdrNominalPeakLevel->setValue(hdrNominalPeakLevel); + + SetComboByValue(ui->ipFamily, ipFamily); + if (!SetComboByValue(ui->bindToIP, bindIP)) + SetInvalidValue(ui->bindToIP, bindIP, bindIP); + + if (obs_video_active()) { + ui->advancedVideoContainer->setEnabled(false); + } + +#ifdef __APPLE__ + bool disableOSXVSync = config_get_bool(App()->GetAppConfig(), "Video", "DisableOSXVSync"); + bool resetOSXVSync = config_get_bool(App()->GetAppConfig(), "Video", "ResetOSXVSyncOnExit"); + ui->disableOSXVSync->setChecked(disableOSXVSync); + ui->resetOSXVSync->setChecked(resetOSXVSync); + ui->resetOSXVSync->setEnabled(disableOSXVSync); +#elif _WIN32 + bool disableAudioDucking = config_get_bool(App()->GetAppConfig(), "Audio", "DisableAudioDucking"); + ui->disableAudioDucking->setChecked(disableAudioDucking); + + const char *processPriority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); + + int idx = ui->processPriority->findData(processPriority); + if (idx == -1) + idx = ui->processPriority->findData("Normal"); + ui->processPriority->setCurrentIndex(idx); + + ui->enableNewSocketLoop->setChecked(enableNewSocketLoop); + ui->enableLowLatencyMode->setChecked(enableLowLatencyMode); + ui->enableLowLatencyMode->setToolTip(QTStr("Basic.Settings.Advanced.Network.TCPPacing.Tooltip")); +#endif +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = config_get_bool(App()->GetAppConfig(), "General", "BrowserHWAccel"); + ui->browserHWAccel->setChecked(browserHWAccel); + prevBrowserAccel = ui->browserHWAccel->isChecked(); +#endif + + SetComboByValue(ui->hotkeyFocusType, hotkeyFocusType); + + loading = false; +} + +template +static inline void LayoutHotkey(OBSBasicSettings *settings, obs_hotkey_id id, obs_hotkey_t *key, Func &&fun, + const map> &keys) +{ + auto *label = new OBSHotkeyLabel; + QString text = QT_UTF8(obs_hotkey_get_description(key)); + + label->setProperty("fullName", text); + TruncateLabel(label, text); + + OBSHotkeyWidget *hw = nullptr; + + auto combos = keys.find(id); + if (combos == std::end(keys)) + hw = new OBSHotkeyWidget(settings, id, obs_hotkey_get_name(key), settings); + else + hw = new OBSHotkeyWidget(settings, id, obs_hotkey_get_name(key), settings, combos->second); + + hw->label = label; + hw->setAccessibleName(text); + label->widget = hw; + + fun(key, label, hw); +} + +template static QLabel *makeLabel(T &t, Func &&getName) +{ + QLabel *label = new QLabel(getName(t)); + label->setStyleSheet("font-weight: bold;"); + return label; +} + +template static QLabel *makeLabel(const OBSSource &source, Func &&) +{ + OBSSourceLabel *label = new OBSSourceLabel(source); + label->setStyleSheet("font-weight: bold;"); + QString name = QT_UTF8(obs_source_get_name(source)); + TruncateLabel(label, name); + + return label; +} + +template +static inline void AddHotkeys(QFormLayout &layout, Func &&getName, + std::vector, QPointer>> &hotkeys) +{ + if (hotkeys.empty()) + return; + + layout.setItem(layout.rowCount(), QFormLayout::SpanningRole, new QSpacerItem(0, 10)); + + using tuple_type = std::tuple, QPointer>; + + stable_sort(begin(hotkeys), end(hotkeys), [&](const tuple_type &a, const tuple_type &b) { + const auto &o_a = get<0>(a); + const auto &o_b = get<0>(b); + return o_a != o_b && string(getName(o_a)) < getName(o_b); + }); + + string prevName; + for (const auto &hotkey : hotkeys) { + const auto &o = get<0>(hotkey); + const char *name = getName(o); + if (prevName != name) { + prevName = name; + layout.setItem(layout.rowCount(), QFormLayout::SpanningRole, new QSpacerItem(0, 10)); + layout.addRow(makeLabel(o, getName)); + } + + auto hlabel = get<1>(hotkey); + auto widget = get<2>(hotkey); + layout.addRow(hlabel, widget); + } +} + +void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey) +{ + hotkeys.clear(); + if (ui->hotkeyFormLayout->rowCount() > 0) { + QLayoutItem *forDeletion = ui->hotkeyFormLayout->takeAt(0); + forDeletion->widget()->deleteLater(); + delete forDeletion; + } + ui->hotkeyFilterSearch->blockSignals(true); + ui->hotkeyFilterInput->blockSignals(true); + ui->hotkeyFilterSearch->setText(""); + ui->hotkeyFilterInput->ResetKey(); + ui->hotkeyFilterSearch->blockSignals(false); + ui->hotkeyFilterInput->blockSignals(false); + + using keys_t = map>; + keys_t keys; + obs_enum_hotkey_bindings( + [](void *data, size_t, obs_hotkey_binding_t *binding) { + auto &keys = *static_cast(data); + + keys[obs_hotkey_binding_get_hotkey_id(binding)].emplace_back( + obs_hotkey_binding_get_key_combination(binding)); + + return true; + }, + &keys); + + QFormLayout *hotkeysLayout = new QFormLayout(); + hotkeysLayout->setVerticalSpacing(0); + hotkeysLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + hotkeysLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter); + auto hotkeyChildWidget = new QWidget(); + hotkeyChildWidget->setLayout(hotkeysLayout); + ui->hotkeyFormLayout->addRow(hotkeyChildWidget); + + using namespace std; + using encoders_elem_t = tuple, QPointer>; + using outputs_elem_t = tuple, QPointer>; + using services_elem_t = tuple, QPointer>; + using sources_elem_t = tuple, QPointer>; + vector encoders; + vector outputs; + vector services; + vector scenes; + vector sources; + + vector pairIds; + map> pairLabels; + + using std::move; + + auto HandleEncoder = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { + auto weak_encoder = static_cast(registerer); + auto encoder = OBSGetStrongRef(weak_encoder); + + if (!encoder) + return true; + + encoders.emplace_back(std::move(encoder), label, hw); + return false; + }; + + auto HandleOutput = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { + auto weak_output = static_cast(registerer); + auto output = OBSGetStrongRef(weak_output); + + if (!output) + return true; + + outputs.emplace_back(std::move(output), label, hw); + return false; + }; + + auto HandleService = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { + auto weak_service = static_cast(registerer); + auto service = OBSGetStrongRef(weak_service); + + if (!service) + return true; + + services.emplace_back(std::move(service), label, hw); + return false; + }; + + auto HandleSource = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { + auto weak_source = static_cast(registerer); + auto source = OBSGetStrongRef(weak_source); + + if (!source) + return true; + + if (obs_scene_from_source(source)) + scenes.emplace_back(source, label, hw); + else if (obs_source_get_name(source) != NULL) + sources.emplace_back(source, label, hw); + + return false; + }; + + auto RegisterHotkey = [&](obs_hotkey_t *key, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { + auto registerer_type = obs_hotkey_get_registerer_type(key); + void *registerer = obs_hotkey_get_registerer(key); + + obs_hotkey_id partner = obs_hotkey_get_pair_partner_id(key); + if (partner != OBS_INVALID_HOTKEY_ID) { + pairLabels.emplace(obs_hotkey_get_id(key), make_pair(partner, label)); + pairIds.push_back(obs_hotkey_get_id(key)); + } + + using std::move; + + switch (registerer_type) { + case OBS_HOTKEY_REGISTERER_FRONTEND: + hotkeysLayout->addRow(label, hw); + break; + + case OBS_HOTKEY_REGISTERER_ENCODER: + if (HandleEncoder(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_OUTPUT: + if (HandleOutput(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_SERVICE: + if (HandleService(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_SOURCE: + if (HandleSource(registerer, label, hw)) + return; + break; + } + + hotkeys.emplace_back(registerer_type == OBS_HOTKEY_REGISTERER_FRONTEND, hw); + connect(hw, &OBSHotkeyWidget::KeyChanged, this, [=]() { + HotkeysChanged(); + ScanDuplicateHotkeys(hotkeysLayout); + }); + connect(hw, &OBSHotkeyWidget::SearchKey, [=](obs_key_combination_t combo) { + ui->hotkeyFilterSearch->setText(""); + ui->hotkeyFilterInput->HandleNewKey(combo); + ui->hotkeyFilterInput->KeyChanged(combo); + }); + }; + + auto data = make_tuple(RegisterHotkey, std::move(keys), ignoreKey, this); + using data_t = decltype(data); + obs_enum_hotkeys( + [](void *data, obs_hotkey_id id, obs_hotkey_t *key) { + data_t &d = *static_cast(data); + if (id != get<2>(d)) + LayoutHotkey(get<3>(d), id, key, get<0>(d), get<1>(d)); + return true; + }, + &data); + + for (auto keyId : pairIds) { + auto data1 = pairLabels.find(keyId); + if (data1 == end(pairLabels)) + continue; + + auto &label1 = data1->second.second; + if (label1->pairPartner) + continue; + + auto data2 = pairLabels.find(data1->second.first); + if (data2 == end(pairLabels)) + continue; + + auto &label2 = data2->second.second; + if (label2->pairPartner) + continue; + + QString tt = QTStr("Basic.Settings.Hotkeys.Pair"); + auto name1 = label1->text(); + auto name2 = label2->text(); + + auto Update = [&](OBSHotkeyLabel *label, const QString &name, OBSHotkeyLabel *other, + const QString &otherName) { + QString string = other->property("fullName").value(); + + if (string.isEmpty() || string.isNull()) + string = otherName; + + label->setToolTip(tt.arg(string)); + label->setText(name + " *"); + label->pairPartner = other; + }; + Update(label1, name1, label2, name2); + Update(label2, name2, label1, name1); + } + + AddHotkeys(*hotkeysLayout, obs_output_get_name, outputs); + AddHotkeys(*hotkeysLayout, obs_source_get_name, scenes); + AddHotkeys(*hotkeysLayout, obs_source_get_name, sources); + AddHotkeys(*hotkeysLayout, obs_encoder_get_name, encoders); + AddHotkeys(*hotkeysLayout, obs_service_get_name, services); + + ScanDuplicateHotkeys(hotkeysLayout); + + /* After this function returns the UI can still be unresponsive for a bit. + * So by deferring the call to unsetCursor() to the Qt event loop it will + * take until it has actually finished processing the created widgets + * before the cursor is reset. */ + QTimer::singleShot(1, this, &OBSBasicSettings::unsetCursor); + hotkeysLoaded = true; +} + +void OBSBasicSettings::LoadSettings(bool changedOnly) +{ + if (!changedOnly || generalChanged) + LoadGeneralSettings(); + if (!changedOnly || stream1Changed) + LoadStream1Settings(); + if (!changedOnly || outputsChanged) + LoadOutputSettings(); + if (!changedOnly || audioChanged) + LoadAudioSettings(); + if (!changedOnly || videoChanged) + LoadVideoSettings(); + if (!changedOnly || a11yChanged) + LoadA11ySettings(); + if (!changedOnly || appearanceChanged) + LoadAppearanceSettings(); + if (!changedOnly || advancedChanged) + LoadAdvancedSettings(); +} + +void OBSBasicSettings::SaveGeneralSettings() +{ + int languageIndex = ui->language->currentIndex(); + QVariant langData = ui->language->itemData(languageIndex); + string language = langData.toString().toStdString(); + + if (WidgetChanged(ui->language)) + config_set_string(App()->GetUserConfig(), "General", "Language", language.c_str()); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + if (WidgetChanged(ui->enableAutoUpdates)) + config_set_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates", + ui->enableAutoUpdates->isChecked()); + int branchIdx = ui->updateChannelBox->currentIndex(); + QString branchName = ui->updateChannelBox->itemData(branchIdx).toString(); + + if (WidgetChanged(ui->updateChannelBox)) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", QT_TO_UTF8(branchName)); + forceUpdateCheck = true; + } +#endif +#ifdef _WIN32 + if (ui->hideOBSFromCapture && WidgetChanged(ui->hideOBSFromCapture)) { + bool hide_window = ui->hideOBSFromCapture->isChecked(); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture", hide_window); + + QWindowList windows = QGuiApplication::allWindows(); + for (auto window : windows) { + if (window->isVisible()) { + main->SetDisplayAffinity(window); + } + } + + blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hide_window ? "true" : "false"); + } +#endif + if (WidgetChanged(ui->openStatsOnStartup)) + config_set_bool(main->Config(), "General", "OpenStatsOnStartup", ui->openStatsOnStartup->isChecked()); + if (WidgetChanged(ui->snappingEnabled)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SnappingEnabled", + ui->snappingEnabled->isChecked()); + if (WidgetChanged(ui->screenSnapping)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ScreenSnapping", + ui->screenSnapping->isChecked()); + if (WidgetChanged(ui->centerSnapping)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "CenterSnapping", + ui->centerSnapping->isChecked()); + if (WidgetChanged(ui->sourceSnapping)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SourceSnapping", + ui->sourceSnapping->isChecked()); + if (WidgetChanged(ui->snapDistance)) + config_set_double(App()->GetUserConfig(), "BasicWindow", "SnapDistance", ui->snapDistance->value()); + if (WidgetChanged(ui->overflowAlwaysVisible) || WidgetChanged(ui->overflowHide) || + WidgetChanged(ui->overflowSelectionHide)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible", + ui->overflowAlwaysVisible->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden", ui->overflowHide->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden", + ui->overflowSelectionHide->isChecked()); + main->UpdatePreviewOverflowSettings(); + } + if (WidgetChanged(ui->previewSafeAreas)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas", + ui->previewSafeAreas->isChecked()); + main->UpdatePreviewSafeAreas(); + } + + if (WidgetChanged(ui->previewSpacingHelpers)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled", + ui->previewSpacingHelpers->isChecked()); + main->UpdatePreviewSpacingHelpers(); + } + + if (WidgetChanged(ui->doubleClickSwitch)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick", + ui->doubleClickSwitch->isChecked()); + if (WidgetChanged(ui->automaticSearch)) + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", + ui->automaticSearch->isChecked()); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream", + ui->warnBeforeStreamStart->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream", + ui->warnBeforeStreamStop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord", + ui->warnBeforeRecordStop->isChecked()); + + if (WidgetChanged(ui->hideProjectorCursor)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "HideProjectorCursor", + ui->hideProjectorCursor->isChecked()); + main->UpdateProjectorHideCursor(); + } + + if (WidgetChanged(ui->projectorAlwaysOnTop)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ProjectorAlwaysOnTop", + ui->projectorAlwaysOnTop->isChecked()); +#if defined(_WIN32) || defined(__APPLE__) + main->UpdateProjectorAlwaysOnTop(ui->projectorAlwaysOnTop->isChecked()); +#else + main->ResetProjectors(); +#endif + } + + if (WidgetChanged(ui->recordWhenStreaming)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming", + ui->recordWhenStreaming->isChecked()); + if (WidgetChanged(ui->keepRecordStreamStops)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops", + ui->keepRecordStreamStops->isChecked()); + + if (WidgetChanged(ui->replayWhileStreaming)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming", + ui->replayWhileStreaming->isChecked()); + if (WidgetChanged(ui->keepReplayStreamStops)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops", + ui->keepReplayStreamStops->isChecked()); + + if (WidgetChanged(ui->systemTrayEnabled)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled", + ui->systemTrayEnabled->isChecked()); + + main->SystemTray(false); + } + + if (WidgetChanged(ui->systemTrayWhenStarted)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted", + ui->systemTrayWhenStarted->isChecked()); + + if (WidgetChanged(ui->systemTrayAlways)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray", + ui->systemTrayAlways->isChecked()); + + if (WidgetChanged(ui->saveProjectors)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors", + ui->saveProjectors->isChecked()); + + if (WidgetChanged(ui->closeProjectors)) + config_set_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors", + ui->closeProjectors->isChecked()); + + if (WidgetChanged(ui->studioPortraitLayout)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout", + ui->studioPortraitLayout->isChecked()); + + main->ResetUI(); + } + + if (WidgetChanged(ui->prevProgLabelToggle)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels", + ui->prevProgLabelToggle->isChecked()); + + main->ResetUI(); + } + + bool multiviewChanged = false; + if (WidgetChanged(ui->multiviewMouseSwitch)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewMouseSwitch", + ui->multiviewMouseSwitch->isChecked()); + multiviewChanged = true; + } + + if (WidgetChanged(ui->multiviewDrawNames)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawNames", + ui->multiviewDrawNames->isChecked()); + multiviewChanged = true; + } + + if (WidgetChanged(ui->multiviewDrawAreas)) { + config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawAreas", + ui->multiviewDrawAreas->isChecked()); + multiviewChanged = true; + } + + if (WidgetChanged(ui->multiviewLayout)) { + config_set_int(App()->GetUserConfig(), "BasicWindow", "MultiviewLayout", + ui->multiviewLayout->currentData().toInt()); + multiviewChanged = true; + } + + if (multiviewChanged) + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasicSettings::SaveVideoSettings() +{ + QString baseResolution = ui->baseResolution->currentText(); + QString outputResolution = ui->outputResolution->currentText(); + int fpsType = ui->fpsType->currentIndex(); + uint32_t cx = 0, cy = 0; + + /* ------------------- */ + + if (WidgetChanged(ui->baseResolution) && ConvertResText(QT_TO_UTF8(baseResolution), cx, cy)) { + config_set_uint(main->Config(), "Video", "BaseCX", cx); + config_set_uint(main->Config(), "Video", "BaseCY", cy); + } + + if (WidgetChanged(ui->outputResolution) && ConvertResText(QT_TO_UTF8(outputResolution), cx, cy)) { + config_set_uint(main->Config(), "Video", "OutputCX", cx); + config_set_uint(main->Config(), "Video", "OutputCY", cy); + } + + if (WidgetChanged(ui->fpsType)) + config_set_uint(main->Config(), "Video", "FPSType", fpsType); + + SaveCombo(ui->fpsCommon, "Video", "FPSCommon"); + SaveSpinBox(ui->fpsInteger, "Video", "FPSInt"); + SaveSpinBox(ui->fpsNumerator, "Video", "FPSNum"); + SaveSpinBox(ui->fpsDenominator, "Video", "FPSDen"); + SaveComboData(ui->downscaleFilter, "Video", "ScaleType"); +} + +void OBSBasicSettings::SaveAdvancedSettings() +{ + QString lastMonitoringDevice = config_get_string(main->Config(), "Audio", "MonitoringDeviceId"); + +#ifdef _WIN32 + if (WidgetChanged(ui->renderer)) + config_set_string(App()->GetAppConfig(), "Video", "Renderer", QT_TO_UTF8(ui->renderer->currentText())); + + std::string priority = QT_TO_UTF8(ui->processPriority->currentData().toString()); + config_set_string(App()->GetAppConfig(), "General", "ProcessPriority", priority.c_str()); + if (main->Active()) + SetProcessPriority(priority.c_str()); + + SaveCheckBox(ui->enableNewSocketLoop, "Output", "NewSocketLoopEnable"); + SaveCheckBox(ui->enableLowLatencyMode, "Output", "LowLatencyEnable"); +#endif +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = ui->browserHWAccel->isChecked(); + config_set_bool(App()->GetAppConfig(), "General", "BrowserHWAccel", browserHWAccel); +#endif + + if (WidgetChanged(ui->hotkeyFocusType)) { + QString str = GetComboData(ui->hotkeyFocusType); + config_set_string(App()->GetUserConfig(), "General", "HotkeyFocusType", QT_TO_UTF8(str)); + } + +#ifdef __APPLE__ + if (WidgetChanged(ui->disableOSXVSync)) { + bool disable = ui->disableOSXVSync->isChecked(); + config_set_bool(App()->GetAppConfig(), "Video", "DisableOSXVSync", disable); + EnableOSXVSync(!disable); + } + if (WidgetChanged(ui->resetOSXVSync)) + config_set_bool(App()->GetAppConfig(), "Video", "ResetOSXVSyncOnExit", ui->resetOSXVSync->isChecked()); +#endif + + SaveComboData(ui->colorFormat, "Video", "ColorFormat"); + SaveComboData(ui->colorSpace, "Video", "ColorSpace"); + SaveComboData(ui->colorRange, "Video", "ColorRange"); + SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel"); + SaveSpinBox(ui->hdrNominalPeakLevel, "Video", "HdrNominalPeakLevel"); + if (obs_audio_monitoring_available()) { + SaveCombo(ui->monitoringDevice, "Audio", "MonitoringDeviceName"); + SaveComboData(ui->monitoringDevice, "Audio", "MonitoringDeviceId"); + } + +#ifdef _WIN32 + if (WidgetChanged(ui->disableAudioDucking)) { + bool disable = ui->disableAudioDucking->isChecked(); + config_set_bool(App()->GetAppConfig(), "Audio", "DisableAudioDucking", disable); + DisableAudioDucking(disable); + } +#endif + + if (WidgetChanged(ui->confirmOnExit)) + config_set_bool(App()->GetUserConfig(), "General", "ConfirmOnExit", ui->confirmOnExit->isChecked()); + + SaveEdit(ui->filenameFormatting, "Output", "FilenameFormatting"); + SaveEdit(ui->simpleRBPrefix, "SimpleOutput", "RecRBPrefix"); + SaveEdit(ui->simpleRBSuffix, "SimpleOutput", "RecRBSuffix"); + SaveCheckBox(ui->overwriteIfExists, "Output", "OverwriteIfExists"); + SaveCheckBox(ui->streamDelayEnable, "Output", "DelayEnable"); + SaveSpinBox(ui->streamDelaySec, "Output", "DelaySec"); + SaveCheckBox(ui->streamDelayPreserve, "Output", "DelayPreserve"); + SaveCheckBox(ui->reconnectEnable, "Output", "Reconnect"); + SaveSpinBox(ui->reconnectRetryDelay, "Output", "RetryDelay"); + SaveSpinBox(ui->reconnectMaxRetries, "Output", "MaxRetries"); + SaveComboData(ui->bindToIP, "Output", "BindIP"); + SaveComboData(ui->ipFamily, "Output", "IPFamily"); + SaveCheckBox(ui->autoRemux, "Video", "AutoRemux"); + SaveCheckBox(ui->dynBitrate, "Output", "DynamicBitrate"); + + if (obs_audio_monitoring_available()) { + QString newDevice = ui->monitoringDevice->currentData().toString(); + + if (lastMonitoringDevice != newDevice) { + obs_set_audio_monitoring_device(QT_TO_UTF8(ui->monitoringDevice->currentText()), + QT_TO_UTF8(newDevice)); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", + QT_TO_UTF8(ui->monitoringDevice->currentText()), QT_TO_UTF8(newDevice)); + } + } +} + +static inline const char *OutputModeFromIdx(int idx) +{ + if (idx == 1) + return "Advanced"; + else + return "Simple"; +} + +static inline const char *RecTypeFromIdx(int idx) +{ + if (idx == 1) + return "FFmpeg"; + else + return "Standard"; +} + +static inline const char *SplitFileTypeFromIdx(int idx) +{ + if (idx == 1) + return "Size"; + else if (idx == 2) + return "Manual"; + else + return "Time"; +} + +static void WriteJsonData(OBSPropertiesView *view, const char *path) +{ + if (!view || !WidgetChanged(view)) + return; + + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(path); + + if (!jsonFilePath.empty()) { + obs_data_t *settings = view->GetSettings(); + if (settings) { + obs_data_save_json_safe(settings, jsonFilePath.u8string().c_str(), "tmp", "bak"); + } + } +} + +static void SaveTrackIndex(config_t *config, const char *section, const char *name, QAbstractButton *check1, + QAbstractButton *check2, QAbstractButton *check3, QAbstractButton *check4, + QAbstractButton *check5, QAbstractButton *check6) +{ + if (check1->isChecked()) + config_set_int(config, section, name, 1); + else if (check2->isChecked()) + config_set_int(config, section, name, 2); + else if (check3->isChecked()) + config_set_int(config, section, name, 3); + else if (check4->isChecked()) + config_set_int(config, section, name, 4); + else if (check5->isChecked()) + config_set_int(config, section, name, 5); + else if (check6->isChecked()) + config_set_int(config, section, name, 6); +} + +void OBSBasicSettings::SaveFormat(QComboBox *combo) +{ + QVariant v = combo->currentData(); + if (!v.isNull()) { + auto format = v.value(); + config_set_string(main->Config(), "AdvOut", "FFFormat", format.name); + config_set_string(main->Config(), "AdvOut", "FFFormatMimeType", format.mime_type); + + const char *ext = format.extensions; + string extStr = ext ? ext : ""; + + char *comma = strchr(&extStr[0], ','); + if (comma) + *comma = 0; + + config_set_string(main->Config(), "AdvOut", "FFExtension", extStr.c_str()); + } else { + config_set_string(main->Config(), "AdvOut", "FFFormat", nullptr); + config_set_string(main->Config(), "AdvOut", "FFFormatMimeType", nullptr); + + config_remove_value(main->Config(), "AdvOut", "FFExtension"); + } +} + +void OBSBasicSettings::SaveEncoder(QComboBox *combo, const char *section, const char *value) +{ + QVariant v = combo->currentData(); + FFmpegCodec cd{}; + if (!v.isNull()) + cd = v.value(); + + config_set_int(main->Config(), section, QT_TO_UTF8(QString("%1Id").arg(value)), cd.id); + if (cd.id != 0) + config_set_string(main->Config(), section, value, cd.name); + else + config_set_string(main->Config(), section, value, nullptr); +} + +void OBSBasicSettings::SaveOutputSettings() +{ + config_set_string(main->Config(), "Output", "Mode", OutputModeFromIdx(ui->outputMode->currentIndex())); + + QString encoder = ui->simpleOutStrEncoder->currentData().toString(); + const char *presetType; + + if (encoder == SIMPLE_ENCODER_QSV) + presetType = "QSVPreset"; + else if (encoder == SIMPLE_ENCODER_QSV_AV1) + presetType = "QSVPreset"; + else if (encoder == SIMPLE_ENCODER_NVENC) + presetType = "NVENCPreset2"; + else if (encoder == SIMPLE_ENCODER_NVENC_AV1) + presetType = "NVENCPreset2"; +#ifdef ENABLE_HEVC + else if (encoder == SIMPLE_ENCODER_AMD_HEVC) + presetType = "AMDPreset"; + else if (encoder == SIMPLE_ENCODER_NVENC_HEVC) + presetType = "NVENCPreset2"; +#endif + else if (encoder == SIMPLE_ENCODER_AMD) + presetType = "AMDPreset"; + else if (encoder == SIMPLE_ENCODER_AMD_AV1) + presetType = "AMDAV1Preset"; + else if (encoder == SIMPLE_ENCODER_APPLE_H264 +#ifdef ENABLE_HEVC + || encoder == SIMPLE_ENCODER_APPLE_HEVC +#endif + ) + /* The Apple encoders don't have presets like the other encoders + do. This only exists to make sure that the x264 preset doesn't + get overwritten with empty data. */ + presetType = "ApplePreset"; + else + presetType = "Preset"; + + SaveSpinBox(ui->simpleOutputVBitrate, "SimpleOutput", "VBitrate"); + SaveComboData(ui->simpleOutStrEncoder, "SimpleOutput", "StreamEncoder"); + SaveComboData(ui->simpleOutStrAEncoder, "SimpleOutput", "StreamAudioEncoder"); + SaveCombo(ui->simpleOutputABitrate, "SimpleOutput", "ABitrate"); + SaveEdit(ui->simpleOutputPath, "SimpleOutput", "FilePath"); + SaveCheckBox(ui->simpleNoSpace, "SimpleOutput", "FileNameWithoutSpace"); + SaveComboData(ui->simpleOutRecFormat, "SimpleOutput", "RecFormat2"); + SaveCheckBox(ui->simpleOutAdvanced, "SimpleOutput", "UseAdvanced"); + SaveComboData(ui->simpleOutPreset, "SimpleOutput", presetType); + SaveEdit(ui->simpleOutCustom, "SimpleOutput", "x264Settings"); + SaveComboData(ui->simpleOutRecQuality, "SimpleOutput", "RecQuality"); + SaveComboData(ui->simpleOutRecEncoder, "SimpleOutput", "RecEncoder"); + SaveComboData(ui->simpleOutRecAEncoder, "SimpleOutput", "RecAudioEncoder"); + SaveEdit(ui->simpleOutMuxCustom, "SimpleOutput", "MuxerCustom"); + SaveGroupBox(ui->simpleReplayBuf, "SimpleOutput", "RecRB"); + SaveSpinBox(ui->simpleRBSecMax, "SimpleOutput", "RecRBTime"); + SaveSpinBox(ui->simpleRBMegsMax, "SimpleOutput", "RecRBSize"); + config_set_int(main->Config(), "SimpleOutput", "RecTracks", SimpleOutGetSelectedAudioTracks()); + + curAdvStreamEncoder = GetComboData(ui->advOutEncoder); + + SaveComboData(ui->advOutEncoder, "AdvOut", "Encoder"); + SaveComboData(ui->advOutAEncoder, "AdvOut", "AudioEncoder"); + SaveCombo(ui->advOutRescale, "AdvOut", "RescaleRes"); + SaveComboData(ui->advOutRescaleFilter, "AdvOut", "RescaleFilter"); + SaveTrackIndex(main->Config(), "AdvOut", "TrackIndex", ui->advOutTrack1, ui->advOutTrack2, ui->advOutTrack3, + ui->advOutTrack4, ui->advOutTrack5, ui->advOutTrack6); + config_set_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes", AdvOutGetStreamingSelectedAudioTracks()); + config_set_string(main->Config(), "AdvOut", "RecType", RecTypeFromIdx(ui->advOutRecType->currentIndex())); + + curAdvRecordEncoder = GetComboData(ui->advOutRecEncoder); + + SaveEdit(ui->advOutRecPath, "AdvOut", "RecFilePath"); + SaveCheckBox(ui->advOutNoSpace, "AdvOut", "RecFileNameWithoutSpace"); + SaveComboData(ui->advOutRecFormat, "AdvOut", "RecFormat2"); + SaveComboData(ui->advOutRecEncoder, "AdvOut", "RecEncoder"); + SaveComboData(ui->advOutRecAEncoder, "AdvOut", "RecAudioEncoder"); + SaveCombo(ui->advOutRecRescale, "AdvOut", "RecRescaleRes"); + SaveComboData(ui->advOutRecRescaleFilter, "AdvOut", "RecRescaleFilter"); + SaveEdit(ui->advOutMuxCustom, "AdvOut", "RecMuxerCustom"); + SaveCheckBox(ui->advOutSplitFile, "AdvOut", "RecSplitFile"); + config_set_string(main->Config(), "AdvOut", "RecSplitFileType", + SplitFileTypeFromIdx(ui->advOutSplitFileType->currentIndex())); + SaveSpinBox(ui->advOutSplitFileTime, "AdvOut", "RecSplitFileTime"); + SaveSpinBox(ui->advOutSplitFileSize, "AdvOut", "RecSplitFileSize"); + + config_set_int(main->Config(), "AdvOut", "RecTracks", AdvOutGetSelectedAudioTracks()); + + config_set_int(main->Config(), "AdvOut", "FLVTrack", CurrentFLVTrack()); + + config_set_bool(main->Config(), "AdvOut", "FFOutputToFile", + ui->advOutFFType->currentIndex() == 0 ? true : false); + SaveEdit(ui->advOutFFRecPath, "AdvOut", "FFFilePath"); + SaveCheckBox(ui->advOutFFNoSpace, "AdvOut", "FFFileNameWithoutSpace"); + SaveEdit(ui->advOutFFURL, "AdvOut", "FFURL"); + SaveFormat(ui->advOutFFFormat); + SaveEdit(ui->advOutFFMCfg, "AdvOut", "FFMCustom"); + SaveSpinBox(ui->advOutFFVBitrate, "AdvOut", "FFVBitrate"); + SaveSpinBox(ui->advOutFFVGOPSize, "AdvOut", "FFVGOPSize"); + SaveCheckBox(ui->advOutFFUseRescale, "AdvOut", "FFRescale"); + SaveCheckBox(ui->advOutFFIgnoreCompat, "AdvOut", "FFIgnoreCompat"); + SaveCombo(ui->advOutFFRescale, "AdvOut", "FFRescaleRes"); + SaveEncoder(ui->advOutFFVEncoder, "AdvOut", "FFVEncoder"); + SaveEdit(ui->advOutFFVCfg, "AdvOut", "FFVCustom"); + SaveSpinBox(ui->advOutFFABitrate, "AdvOut", "FFABitrate"); + SaveEncoder(ui->advOutFFAEncoder, "AdvOut", "FFAEncoder"); + SaveEdit(ui->advOutFFACfg, "AdvOut", "FFACustom"); + config_set_int(main->Config(), "AdvOut", "FFAudioMixes", + (ui->advOutFFTrack1->isChecked() ? (1 << 0) : 0) | + (ui->advOutFFTrack2->isChecked() ? (1 << 1) : 0) | + (ui->advOutFFTrack3->isChecked() ? (1 << 2) : 0) | + (ui->advOutFFTrack4->isChecked() ? (1 << 3) : 0) | + (ui->advOutFFTrack5->isChecked() ? (1 << 4) : 0) | + (ui->advOutFFTrack6->isChecked() ? (1 << 5) : 0)); + SaveCombo(ui->advOutTrack1Bitrate, "AdvOut", "Track1Bitrate"); + SaveCombo(ui->advOutTrack2Bitrate, "AdvOut", "Track2Bitrate"); + SaveCombo(ui->advOutTrack3Bitrate, "AdvOut", "Track3Bitrate"); + SaveCombo(ui->advOutTrack4Bitrate, "AdvOut", "Track4Bitrate"); + SaveCombo(ui->advOutTrack5Bitrate, "AdvOut", "Track5Bitrate"); + SaveCombo(ui->advOutTrack6Bitrate, "AdvOut", "Track6Bitrate"); + SaveEdit(ui->advOutTrack1Name, "AdvOut", "Track1Name"); + SaveEdit(ui->advOutTrack2Name, "AdvOut", "Track2Name"); + SaveEdit(ui->advOutTrack3Name, "AdvOut", "Track3Name"); + SaveEdit(ui->advOutTrack4Name, "AdvOut", "Track4Name"); + SaveEdit(ui->advOutTrack5Name, "AdvOut", "Track5Name"); + SaveEdit(ui->advOutTrack6Name, "AdvOut", "Track6Name"); + + if (vodTrackCheckbox) { + SaveCheckBox(simpleVodTrack, "SimpleOutput", "VodTrackEnabled"); + SaveCheckBox(vodTrackCheckbox, "AdvOut", "VodTrackEnabled"); + SaveTrackIndex(main->Config(), "AdvOut", "VodTrackIndex", vodTrack[0], vodTrack[1], vodTrack[2], + vodTrack[3], vodTrack[4], vodTrack[5]); + } + + SaveCheckBox(ui->advReplayBuf, "AdvOut", "RecRB"); + SaveSpinBox(ui->advRBSecMax, "AdvOut", "RecRBTime"); + SaveSpinBox(ui->advRBMegsMax, "AdvOut", "RecRBSize"); + + WriteJsonData(streamEncoderProps, "streamEncoder.json"); + WriteJsonData(recordEncoderProps, "recordEncoder.json"); + main->ResetOutputs(); +} + +void OBSBasicSettings::SaveAudioSettings() +{ + QString sampleRateStr = ui->sampleRate->currentText(); + int channelSetupIdx = ui->channelSetup->currentIndex(); + + const char *channelSetup; + switch (channelSetupIdx) { + case 0: + channelSetup = "Mono"; + break; + case 1: + channelSetup = "Stereo"; + break; + case 2: + channelSetup = "2.1"; + break; + case 3: + channelSetup = "4.0"; + break; + case 4: + channelSetup = "4.1"; + break; + case 5: + channelSetup = "5.1"; + break; + case 6: + channelSetup = "7.1"; + break; + + default: + channelSetup = "Stereo"; + break; + } + + int sampleRate = 44100; + if (sampleRateStr == "48 kHz") + sampleRate = 48000; + + if (WidgetChanged(ui->sampleRate)) + config_set_uint(main->Config(), "Audio", "SampleRate", sampleRate); + + if (WidgetChanged(ui->channelSetup)) + config_set_string(main->Config(), "Audio", "ChannelSetup", channelSetup); + + if (WidgetChanged(ui->meterDecayRate)) { + double meterDecayRate; + switch (ui->meterDecayRate->currentIndex()) { + case 0: + meterDecayRate = VOLUME_METER_DECAY_FAST; + break; + case 1: + meterDecayRate = VOLUME_METER_DECAY_MEDIUM; + break; + case 2: + meterDecayRate = VOLUME_METER_DECAY_SLOW; + break; + default: + meterDecayRate = VOLUME_METER_DECAY_FAST; + break; + } + config_set_double(main->Config(), "Audio", "MeterDecayRate", meterDecayRate); + + main->UpdateVolumeControlsDecayRate(); + } + + if (WidgetChanged(ui->peakMeterType)) { + uint32_t peakMeterTypeIdx = ui->peakMeterType->currentIndex(); + config_set_uint(main->Config(), "Audio", "PeakMeterType", peakMeterTypeIdx); + + main->UpdateVolumeControlsPeakMeterType(); + } + + if (WidgetChanged(ui->lowLatencyBuffering)) { + bool enableLLAudioBuffering = ui->lowLatencyBuffering->isChecked(); + config_set_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering", enableLLAudioBuffering); + } + + for (auto &audioSource : audioSources) { + auto source = OBSGetStrongRef(get<0>(audioSource)); + if (!source) + continue; + + auto &ptmCB = get<1>(audioSource); + auto &ptmSB = get<2>(audioSource); + auto &pttCB = get<3>(audioSource); + auto &pttSB = get<4>(audioSource); + + obs_source_enable_push_to_mute(source, ptmCB->isChecked()); + obs_source_set_push_to_mute_delay(source, ptmSB->value()); + + obs_source_enable_push_to_talk(source, pttCB->isChecked()); + obs_source_set_push_to_talk_delay(source, pttSB->value()); + } + + auto UpdateAudioDevice = [this](bool input, QComboBox *combo, const char *name, int index) { + main->ResetAudioDevice(input ? App()->InputAudioSource() : App()->OutputAudioSource(), + QT_TO_UTF8(GetComboData(combo)), Str(name), index); + }; + + UpdateAudioDevice(false, ui->desktopAudioDevice1, "Basic.DesktopDevice1", 1); + UpdateAudioDevice(false, ui->desktopAudioDevice2, "Basic.DesktopDevice2", 2); + UpdateAudioDevice(true, ui->auxAudioDevice1, "Basic.AuxDevice1", 3); + UpdateAudioDevice(true, ui->auxAudioDevice2, "Basic.AuxDevice2", 4); + UpdateAudioDevice(true, ui->auxAudioDevice3, "Basic.AuxDevice3", 5); + UpdateAudioDevice(true, ui->auxAudioDevice4, "Basic.AuxDevice4", 6); + main->SaveProject(); +} + +void OBSBasicSettings::SaveHotkeySettings() +{ + const auto &config = main->Config(); + + using namespace std; + + std::vector combinations; + for (auto &hotkey : hotkeys) { + auto &hw = *hotkey.second; + if (!hw.Changed()) + continue; + + hw.Save(combinations); + + if (!hotkey.first) + continue; + + OBSDataArrayAutoRelease array = obs_hotkey_save(hw.id); + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "bindings", array); + const char *json = obs_data_get_json(data); + config_set_string(config, "Hotkeys", hw.name.c_str(), json); + } + + if (!main->outputHandler || !main->outputHandler->replayBuffer) + return; + + const char *id = obs_obj_get_id(main->outputHandler->replayBuffer); + if (strcmp(id, "replay_buffer") == 0) { + OBSDataAutoRelease hotkeys = obs_hotkeys_save_output(main->outputHandler->replayBuffer); + config_set_string(config, "Hotkeys", "ReplayBuffer", obs_data_get_json(hotkeys)); + } +} + +#define MINOR_SEPARATOR "------------------------------------------------" + +static void AddChangedVal(std::string &changed, const char *str) +{ + if (changed.size()) + changed += ", "; + changed += str; +} + +void OBSBasicSettings::SaveSettings() +{ + if (generalChanged) + SaveGeneralSettings(); + if (stream1Changed) + SaveStream1Settings(); + if (outputsChanged) + SaveOutputSettings(); + if (audioChanged) + SaveAudioSettings(); + if (videoChanged) + SaveVideoSettings(); + if (hotkeysChanged) + SaveHotkeySettings(); + if (a11yChanged) + SaveA11ySettings(); + if (advancedChanged) + SaveAdvancedSettings(); + if (appearanceChanged) + SaveAppearanceSettings(); + if (videoChanged || advancedChanged) + main->ResetVideo(); + + config_save_safe(main->Config(), "tmp", nullptr); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + main->SaveProject(); + + if (Changed()) { + std::string changed; + if (generalChanged) + AddChangedVal(changed, "general"); + if (stream1Changed) + AddChangedVal(changed, "stream 1"); + if (outputsChanged) + AddChangedVal(changed, "outputs"); + if (audioChanged) + AddChangedVal(changed, "audio"); + if (videoChanged) + AddChangedVal(changed, "video"); + if (hotkeysChanged) + AddChangedVal(changed, "hotkeys"); + if (a11yChanged) + AddChangedVal(changed, "a11y"); + if (appearanceChanged) + AddChangedVal(changed, "appearance"); + if (advancedChanged) + AddChangedVal(changed, "advanced"); + + blog(LOG_INFO, "Settings changed (%s)", changed.c_str()); + blog(LOG_INFO, MINOR_SEPARATOR); + } + + bool langChanged = (ui->language->currentIndex() != prevLangIndex); + bool audioRestart = + (ui->channelSetup->currentIndex() != channelIndex || ui->sampleRate->currentIndex() != sampleRateIndex); + bool browserHWAccelChanged = (ui->browserHWAccel && ui->browserHWAccel->isChecked() != prevBrowserAccel); + + if (langChanged || audioRestart || browserHWAccelChanged) + restart = true; + else + restart = false; +} + +bool OBSBasicSettings::QueryChanges() +{ + QMessageBox::StandardButton button; + + button = OBSMessageBox::question(this, QTStr("Basic.Settings.ConfirmTitle"), QTStr("Basic.Settings.Confirm"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + + if (button == QMessageBox::Cancel) { + return false; + } else if (button == QMessageBox::Yes) { + if (!QueryAllowedToClose()) + return false; + + SaveSettings(); + } else { + if (savedTheme != App()->GetTheme()) + App()->SetTheme(savedTheme->id); + + LoadSettings(true); + restart = false; + } + + ClearChanged(); + return true; +} + +bool OBSBasicSettings::QueryAllowedToClose() +{ + bool simple = (ui->outputMode->currentIndex() == 0); + + bool invalidEncoder = false; + bool invalidFormat = false; + bool invalidTracks = false; + if (simple) { + if (ui->simpleOutRecEncoder->currentIndex() == -1 || ui->simpleOutStrEncoder->currentIndex() == -1 || + ui->simpleOutRecAEncoder->currentIndex() == -1 || ui->simpleOutStrAEncoder->currentIndex() == -1) + invalidEncoder = true; + + if (ui->simpleOutRecFormat->currentIndex() == -1) + invalidFormat = true; + + QString qual = ui->simpleOutRecQuality->currentData().toString(); + QString format = ui->simpleOutRecFormat->currentData().toString(); + if (SimpleOutGetSelectedAudioTracks() == 0 && qual != "Stream" && format != "flv") + invalidTracks = true; + } else { + if (ui->advOutRecEncoder->currentIndex() == -1 || ui->advOutEncoder->currentIndex() == -1 || + ui->advOutRecAEncoder->currentIndex() == -1 || ui->advOutAEncoder->currentIndex() == -1) + invalidEncoder = true; + + QString format = ui->advOutRecFormat->currentData().toString(); + if (AdvOutGetSelectedAudioTracks() == 0 && format != "flv") + invalidTracks = true; + if (AdvOutGetStreamingSelectedAudioTracks() == 0) + invalidTracks = true; + } + + if (invalidEncoder) { + OBSMessageBox::warning(this, QTStr("CodecCompat.CodecMissingOnExit.Title"), + QTStr("CodecCompat.CodecMissingOnExit.Text")); + return false; + } else if (invalidFormat) { + OBSMessageBox::warning(this, QTStr("CodecCompat.ContainerMissingOnExit.Title"), + QTStr("CodecCompat.ContainerMissingOnExit.Text")); + return false; + } else if (invalidTracks) { + OBSMessageBox::warning(this, QTStr("OutputWarnings.NoTracksSelectedOnExit.Title"), + QTStr("OutputWarnings.NoTracksSelectedOnExit.Text")); + return false; + } + + return true; +} + +void OBSBasicSettings::closeEvent(QCloseEvent *event) +{ + if (!AskIfCanCloseSettings()) + event->ignore(); +} + +void OBSBasicSettings::showEvent(QShowEvent *event) +{ + QDialog::showEvent(event); + + /* Reduce the height of the widget area if too tall compared to the screen + * size (e.g., 720p) with potential window decoration (e.g., titlebar). */ + const int titleBarHeight = QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); + const int maxHeight = round(screen()->availableGeometry().height() - titleBarHeight); + if (size().height() >= maxHeight) + resize(size().width(), maxHeight); +} + +void OBSBasicSettings::reject() +{ + if (AskIfCanCloseSettings()) + close(); +} + +void OBSBasicSettings::on_listWidget_itemSelectionChanged() +{ + int row = ui->listWidget->currentRow(); + + if (loading || row == pageIndex) + return; + + if (!hotkeysLoaded && row == Pages::HOTKEYS) { + setCursor(Qt::BusyCursor); + /* Look, I know this /feels/ wrong, but the specific issue we're dealing with + * here means that the UI locks up immediately even when using "invokeMethod". + * So the only way for the user to see the loading message on the page is to + * give the Qt event loop a tiny bit of time to switch to the hotkey page, + * and only then start loading. This could maybe be done by subclassing QWidget + * for the hotkey page and then using showEvent() but I *really* don't want + * to deal with that right now. I've got better things to do with my life + * than to work around this god damn stupid issue for something we'll remove + * soon enough anyway. So this solution it is. */ + QTimer::singleShot(1, this, [&]() { LoadHotkeySettings(); }); + } + + pageIndex = row; +} + +void OBSBasicSettings::UpdateYouTubeAppDockSettings() +{ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + if (cef_js_avail) { + std::string service = ui->service->currentText().toStdString(); + if (IsYouTubeService(service)) { + if (!main->GetYouTubeAppDock()) { + main->NewYouTubeAppDock(); + } + main->GetYouTubeAppDock()->SettingsUpdated(!IsYouTubeService(service) || stream1Changed); + } else { + if (main->GetYouTubeAppDock()) { + main->GetYouTubeAppDock()->AccountDisconnected(); + } + main->DeleteYouTubeAppDock(); + } + } +#endif +} + +void OBSBasicSettings::on_buttonBox_clicked(QAbstractButton *button) +{ + QDialogButtonBox::ButtonRole val = ui->buttonBox->buttonRole(button); + + if (val == QDialogButtonBox::ApplyRole || val == QDialogButtonBox::AcceptRole) { + if (!QueryAllowedToClose()) + return; + + SaveSettings(); + + UpdateYouTubeAppDockSettings(); + ClearChanged(); + } + + if (val == QDialogButtonBox::AcceptRole || val == QDialogButtonBox::RejectRole) { + if (val == QDialogButtonBox::RejectRole) { + if (savedTheme != App()->GetTheme()) + App()->SetTheme(savedTheme->id); + } + ClearChanged(); + close(); + } +} + +void OBSBasicSettings::on_simpleOutputBrowse_clicked() +{ + QString dir = + SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->simpleOutputPath->text()); + if (dir.isEmpty()) + return; + + ui->simpleOutputPath->setText(dir); +} + +void OBSBasicSettings::on_advOutRecPathBrowse_clicked() +{ + QString dir = SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->advOutRecPath->text()); + if (dir.isEmpty()) + return; + + ui->advOutRecPath->setText(dir); +} + +void OBSBasicSettings::on_advOutFFPathBrowse_clicked() +{ + QString dir = SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->advOutRecPath->text()); + if (dir.isEmpty()) + return; + + ui->advOutFFRecPath->setText(dir); +} + +void OBSBasicSettings::on_advOutEncoder_currentIndexChanged() +{ + QString encoder = GetComboData(ui->advOutEncoder); + if (!loading) { + bool loadSettings = encoder == curAdvStreamEncoder; + + delete streamEncoderProps; + streamEncoderProps = CreateEncoderPropertyView(QT_TO_UTF8(encoder), + loadSettings ? "streamEncoder.json" : nullptr, true); + streamEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + ui->advOutEncoderLayout->addWidget(streamEncoderProps); + } + + ui->advOutUseRescale->setVisible(true); + ui->advOutRescale->setVisible(true); +} + +void OBSBasicSettings::on_advOutRecEncoder_currentIndexChanged(int idx) +{ + if (!loading) { + delete recordEncoderProps; + recordEncoderProps = nullptr; + } + + if (idx <= 0) { + ui->advOutRecUseRescale->setVisible(false); + ui->advOutRecRescaleContainer->setVisible(false); + ui->advOutRecEncoderProps->setVisible(false); + return; + } + + QString encoder = GetComboData(ui->advOutRecEncoder); + bool loadSettings = encoder == curAdvRecordEncoder; + + if (!loading) { + recordEncoderProps = CreateEncoderPropertyView(QT_TO_UTF8(encoder), + loadSettings ? "recordEncoder.json" : nullptr, true); + recordEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + ui->advOutRecEncoderProps->layout()->addWidget(recordEncoderProps); + connect(recordEncoderProps, &OBSPropertiesView::Changed, this, + &OBSBasicSettings::AdvReplayBufferChanged); + } + + ui->advOutRecUseRescale->setVisible(true); + ui->advOutRecRescaleContainer->setVisible(true); + ui->advOutRecEncoderProps->setVisible(true); +} + +void OBSBasicSettings::on_advOutFFIgnoreCompat_stateChanged(int) +{ + /* Little hack to reload codecs when checked */ + on_advOutFFFormat_currentIndexChanged(ui->advOutFFFormat->currentIndex()); +} + +#define DEFAULT_CONTAINER_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatDescDef") + +void OBSBasicSettings::on_advOutFFFormat_currentIndexChanged(int idx) +{ + const QVariant itemDataVariant = ui->advOutFFFormat->itemData(idx); + + if (!itemDataVariant.isNull()) { + auto format = itemDataVariant.value(); + SetAdvOutputFFmpegEnablement(FFmpegCodecType::AUDIO, format.HasAudio(), false); + SetAdvOutputFFmpegEnablement(FFmpegCodecType::VIDEO, format.HasVideo(), false); + ReloadCodecs(format); + + ui->advOutFFFormatDesc->setText(format.long_name); + + FFmpegCodec defaultAudioCodecDesc = format.GetDefaultEncoder(FFmpegCodecType::AUDIO); + FFmpegCodec defaultVideoCodecDesc = format.GetDefaultEncoder(FFmpegCodecType::VIDEO); + SelectEncoder(ui->advOutFFAEncoder, defaultAudioCodecDesc.name, defaultAudioCodecDesc.id); + SelectEncoder(ui->advOutFFVEncoder, defaultVideoCodecDesc.name, defaultVideoCodecDesc.id); + } else { + ui->advOutFFAEncoder->blockSignals(true); + ui->advOutFFVEncoder->blockSignals(true); + ui->advOutFFAEncoder->clear(); + ui->advOutFFVEncoder->clear(); + + ui->advOutFFFormatDesc->setText(DEFAULT_CONTAINER_STR); + } +} + +void OBSBasicSettings::on_advOutFFAEncoder_currentIndexChanged(int idx) +{ + const QVariant itemDataVariant = ui->advOutFFAEncoder->itemData(idx); + if (!itemDataVariant.isNull()) { + auto desc = itemDataVariant.value(); + SetAdvOutputFFmpegEnablement(FFmpegCodecType::AUDIO, desc.id != 0 || desc.name != nullptr, true); + } +} + +void OBSBasicSettings::on_advOutFFVEncoder_currentIndexChanged(int idx) +{ + const QVariant itemDataVariant = ui->advOutFFVEncoder->itemData(idx); + if (!itemDataVariant.isNull()) { + auto desc = itemDataVariant.value(); + SetAdvOutputFFmpegEnablement(FFmpegCodecType::VIDEO, desc.id != 0 || desc.name != nullptr, true); + } +} + +void OBSBasicSettings::on_advOutFFType_currentIndexChanged(int idx) +{ + ui->advOutFFNoSpace->setHidden(idx != 0); +} + +void OBSBasicSettings::on_colorFormat_currentIndexChanged(int) +{ + UpdateColorFormatSpaceWarning(); +} + +void OBSBasicSettings::on_colorSpace_currentIndexChanged(int) +{ + UpdateColorFormatSpaceWarning(); +} + +#define INVALID_RES_STR "Basic.Settings.Video.InvalidResolution" + +static bool ValidResolutions(Ui::OBSBasicSettings *ui) +{ + QString baseRes = ui->baseResolution->lineEdit()->text(); + uint32_t cx, cy; + + if (!ConvertResText(QT_TO_UTF8(baseRes), cx, cy)) { + ui->videoMsg->setText(QTStr(INVALID_RES_STR)); + return false; + } + + bool lockedOutRes = !ui->outputResolution->isEditable(); + if (!lockedOutRes) { + QString outRes = ui->outputResolution->lineEdit()->text(); + if (!ConvertResText(QT_TO_UTF8(outRes), cx, cy)) { + ui->videoMsg->setText(QTStr(INVALID_RES_STR)); + return false; + } + } + + ui->videoMsg->setText(""); + return true; +} + +void OBSBasicSettings::RecalcOutputResPixels(const char *resText) +{ + uint32_t newCX; + uint32_t newCY; + + if (ConvertResText(resText, newCX, newCY) && newCX && newCY) { + outputCX = newCX; + outputCY = newCY; + + std::tuple aspect = aspect_ratio(outputCX, outputCY); + + ui->scaledAspect->setText( + QTStr("AspectRatio") + .arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); + } +} + +bool OBSBasicSettings::AskIfCanCloseSettings() +{ + bool canCloseSettings = false; + + if (!Changed() || QueryChanges()) + canCloseSettings = true; + + if (forceAuthReload) { + main->auth->Save(); + main->auth->Load(); + forceAuthReload = false; + } + + if (forceUpdateCheck) { + main->CheckForUpdates(false); + forceUpdateCheck = false; + } + + return canCloseSettings; +} + +void OBSBasicSettings::on_filenameFormatting_textEdited(const QString &text) +{ + QString safeStr = text; + +#ifdef __APPLE__ + safeStr.replace(QRegularExpression("[:]"), ""); +#elif defined(_WIN32) + safeStr.replace(QRegularExpression("[<>:\"\\|\\?\\*]"), ""); +#else + // TODO: Add filtering for other platforms +#endif + + if (text != safeStr) + ui->filenameFormatting->setText(safeStr); +} + +void OBSBasicSettings::on_outputResolution_editTextChanged(const QString &text) +{ + if (!loading) { + RecalcOutputResPixels(QT_TO_UTF8(text)); + LoadDownscaleFilters(); + } +} + +void OBSBasicSettings::on_baseResolution_editTextChanged(const QString &text) +{ + if (!loading && ValidResolutions(ui.get())) { + QString baseResolution = text; + uint32_t cx, cy; + + ConvertResText(QT_TO_UTF8(baseResolution), cx, cy); + + std::tuple aspect = aspect_ratio(cx, cy); + + ui->baseAspect->setText( + QTStr("AspectRatio") + .arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); + + ResetDownscales(cx, cy); + } +} + +void OBSBasicSettings::GeneralChanged() +{ + if (!loading) { + generalChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::Stream1Changed() +{ + if (!loading) { + stream1Changed = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::OutputsChanged() +{ + if (!loading) { + outputsChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + + UpdateMultitrackVideo(); + } +} + +void OBSBasicSettings::AudioChanged() +{ + if (!loading) { + audioChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::AudioChangedRestart() +{ + ui->audioMsg->setVisible(false); + + if (!loading) { + int currentChannelIndex = ui->channelSetup->currentIndex(); + int currentSampleRateIndex = ui->sampleRate->currentIndex(); + bool currentLLAudioBufVal = ui->lowLatencyBuffering->isChecked(); + + if (currentChannelIndex != channelIndex || currentSampleRateIndex != sampleRateIndex || + currentLLAudioBufVal != llBufferingEnabled) { + ui->audioMsg->setText(QTStr("Basic.Settings.ProgramRestart")); + ui->audioMsg->setVisible(true); + } else { + ui->audioMsg->setText(""); + } + + audioChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::ReloadAudioSources() +{ + LoadAudioSources(); +} + +#define MULTI_CHANNEL_WARNING "Basic.Settings.Audio.MultichannelWarning" + +void OBSBasicSettings::SpeakerLayoutChanged(int idx) +{ + QString speakerLayoutQstr = ui->channelSetup->itemText(idx); + std::string speakerLayout = QT_TO_UTF8(speakerLayoutQstr); + bool surround = IsSurround(speakerLayout.c_str()); + bool isOpus = ui->simpleOutStrAEncoder->currentData().toString() == "opus"; + + if (surround) { + /* + * Display all bitrates + */ + PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); + + string stream_encoder_id = ui->advOutAEncoder->currentData().toString().toStdString(); + string record_encoder_id = ui->advOutRecAEncoder->currentData().toString().toStdString(); + PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, + ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + stream_encoder_id.c_str(), + record_encoder_id == "none" ? stream_encoder_id.c_str() + : record_encoder_id.c_str()); + } else { + /* + * Reset audio bitrate for simple and adv mode, update list of + * bitrates and save setting. + */ + RestrictResetBitrates({ui->simpleOutputABitrate, ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, + ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, + ui->advOutTrack6Bitrate}, + 320); + + SaveCombo(ui->simpleOutputABitrate, "SimpleOutput", "ABitrate"); + SaveCombo(ui->advOutTrack1Bitrate, "AdvOut", "Track1Bitrate"); + SaveCombo(ui->advOutTrack2Bitrate, "AdvOut", "Track2Bitrate"); + SaveCombo(ui->advOutTrack3Bitrate, "AdvOut", "Track3Bitrate"); + SaveCombo(ui->advOutTrack4Bitrate, "AdvOut", "Track4Bitrate"); + SaveCombo(ui->advOutTrack5Bitrate, "AdvOut", "Track5Bitrate"); + SaveCombo(ui->advOutTrack6Bitrate, "AdvOut", "Track6Bitrate"); + } + + UpdateAudioWarnings(); +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +void OBSBasicSettings::HideOBSWindowWarning(Qt::CheckState state) +#else +void OBSBasicSettings::HideOBSWindowWarning(int state) +#endif +{ + if (loading || state == Qt::Unchecked) + return; + + if (config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutHideOBSFromCapture")) + return; + + OBSMessageBox::information(this, QTStr("Basic.Settings.General.HideOBSWindowsFromCapture"), + QTStr("Basic.Settings.General.HideOBSWindowsFromCapture.Message")); + + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutHideOBSFromCapture", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); +} + +/* + * resets current bitrate if too large and restricts the number of bitrates + * displayed when multichannel OFF + */ + +void RestrictResetBitrates(initializer_list boxes, int maxbitrate) +{ + for (auto box : boxes) { + int idx = box->currentIndex(); + int max_bitrate = FindClosestAvailableAudioBitrate(box, maxbitrate); + int count = box->count(); + int max_idx = box->findText(QT_UTF8(std::to_string(max_bitrate).c_str())); + + for (int i = (count - 1); i > max_idx; i--) + box->removeItem(i); + + if (idx > max_idx) { + int default_bitrate = FindClosestAvailableAudioBitrate(box, maxbitrate / 2); + int default_idx = box->findText(QT_UTF8(std::to_string(default_bitrate).c_str())); + + box->setCurrentIndex(default_idx); + box->setProperty("changed", QVariant(true)); + } else { + box->setCurrentIndex(idx); + } + } +} + +void OBSBasicSettings::AdvancedChangedRestart() +{ + ui->advancedMsg->setVisible(false); + + if (!loading) { + advancedChanged = true; + ui->advancedMsg->setText(QTStr("Basic.Settings.ProgramRestart")); + ui->advancedMsg->setVisible(true); + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::VideoChangedResolution() +{ + if (!loading && ValidResolutions(ui.get())) { + videoChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::VideoChanged() +{ + if (!loading) { + videoChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::HotkeysChanged() +{ + using namespace std; + if (loading) + return; + + hotkeysChanged = any_of(begin(hotkeys), end(hotkeys), [](const pair> &hotkey) { + const auto &hw = *hotkey.second; + return hw.Changed(); + }); + + if (hotkeysChanged) + EnableApplyButton(true); +} + +void OBSBasicSettings::SearchHotkeys(const QString &text, obs_key_combination_t filterCombo) +{ + + if (ui->hotkeyFormLayout->rowCount() == 0) + return; + + std::vector combos; + bool showHotkey; + ui->hotkeyScrollArea->ensureVisible(0, 0); + + QLayoutItem *hotkeysItem = ui->hotkeyFormLayout->itemAt(0); + QWidget *hotkeys = hotkeysItem->widget(); + if (!hotkeys) + return; + + QFormLayout *hotkeysLayout = qobject_cast(hotkeys->layout()); + hotkeysLayout->setEnabled(false); + + QString needle = text.toLower(); + + for (int i = 0; i < hotkeysLayout->rowCount(); i++) { + auto label = hotkeysLayout->itemAt(i, QFormLayout::LabelRole); + if (!label) + continue; + + OBSHotkeyLabel *item = qobject_cast(label->widget()); + if (!item) + continue; + + QString fullname = item->property("fullName").value(); + + showHotkey = needle.isEmpty() || fullname.toLower().contains(needle); + + if (showHotkey && !obs_key_combination_is_empty(filterCombo)) { + showHotkey = false; + + item->widget->GetCombinations(combos); + for (auto combo : combos) { + if (combo == filterCombo) { + showHotkey = true; + break; + } + } + } + + label->widget()->setVisible(showHotkey); + + auto field = hotkeysLayout->itemAt(i, QFormLayout::FieldRole); + if (field) + field->widget()->setVisible(showHotkey); + } + hotkeysLayout->setEnabled(true); +} + +void OBSBasicSettings::on_hotkeyFilterReset_clicked() +{ + ui->hotkeyFilterSearch->setText(""); + ui->hotkeyFilterInput->ResetKey(); +} + +void OBSBasicSettings::on_hotkeyFilterSearch_textChanged(const QString text) +{ + SearchHotkeys(text, ui->hotkeyFilterInput->key); +} + +void OBSBasicSettings::on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo) +{ + SearchHotkeys(ui->hotkeyFilterSearch->text(), combo); +} + +namespace std { +template<> struct hash { + size_t operator()(obs_key_combination_t value) const + { + size_t h1 = hash{}(value.modifiers); + size_t h2 = hash{}(value.key); + // Same as boost::hash_combine() + h2 ^= h1 + 0x9e3779b9 + (h2 << 6) + (h2 >> 2); + return h2; + } +}; +} // namespace std + +bool OBSBasicSettings::ScanDuplicateHotkeys(QFormLayout *layout) +{ + typedef struct assignment { + OBSHotkeyLabel *label; + OBSHotkeyEdit *edit; + } assignment; + + unordered_map> assignments; + vector items; + bool hasDupes = false; + + for (int i = 0; i < layout->rowCount(); i++) { + auto label = layout->itemAt(i, QFormLayout::LabelRole); + if (!label) + continue; + OBSHotkeyLabel *item = qobject_cast(label->widget()); + if (!item) + continue; + + items.push_back(item); + + for (auto &edit : item->widget->edits) { + edit->hasDuplicate = false; + + if (obs_key_combination_is_empty(edit->key)) + continue; + + for (assignment &assign : assignments[edit->key]) { + if (item->pairPartner == assign.label) + continue; + + assign.edit->hasDuplicate = true; + edit->hasDuplicate = true; + hasDupes = true; + } + + assignments[edit->key].push_back({item, edit}); + } + } + + for (auto *item : items) + for (auto &edit : item->widget->edits) + edit->UpdateDuplicationState(); + + return hasDupes; +} + +void OBSBasicSettings::ReloadHotkeys(obs_hotkey_id ignoreKey) +{ + if (!hotkeysLoaded) + return; + LoadHotkeySettings(ignoreKey); +} + +void OBSBasicSettings::A11yChanged() +{ + if (!loading) { + a11yChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::AppearanceChanged() +{ + if (!loading) { + appearanceChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::AdvancedChanged() +{ + if (!loading) { + advancedChanged = true; + sender()->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::AdvOutSplitFileChanged() +{ + bool splitFile = ui->advOutSplitFile->isChecked(); + int splitFileType = splitFile ? ui->advOutSplitFileType->currentIndex() : -1; + + ui->advOutSplitFileType->setEnabled(splitFile); + ui->advOutSplitFileTimeLabel->setVisible(splitFileType == 0); + ui->advOutSplitFileTime->setVisible(splitFileType == 0); + ui->advOutSplitFileSizeLabel->setVisible(splitFileType == 1); + ui->advOutSplitFileSize->setVisible(splitFileType == 1); +} + +static void DisableIncompatibleCodecs(QComboBox *cbox, const QString &format, const QString &formatName, + const QString &streamEncoder) +{ + QString strEncLabel = QTStr("Basic.Settings.Output.Adv.Recording.UseStreamEncoder"); + QString recEncoder = cbox->currentData().toString(); + + /* Check if selected encoders and output format are compatible, disable incompatible items. */ + bool currentCompatible = true; + for (int idx = 0; idx < cbox->count(); idx++) { + QString encName = cbox->itemData(idx).toString(); + string encoderId = (encName == "none") ? streamEncoder.toStdString() : encName.toStdString(); + QString encDisplayName = (encName == "none") ? strEncLabel + : obs_encoder_get_display_name(encoderId.c_str()); + + /* Something has gone horribly wrong and there's no encoder */ + if (encoderId.empty()) + continue; + + if (obs_get_encoder_caps(encoderId.c_str()) & OBS_ENCODER_CAP_DEPRECATED) { + encDisplayName += " (" + QTStr("Deprecated") + ")"; + } + + const char *codec = obs_get_encoder_codec(encoderId.c_str()); + + bool is_compatible = ContainerSupportsCodec(format.toStdString(), codec); + /* Fall back to FFmpeg check if codec not one of the built-in ones. */ + if (!is_compatible && !IsBuiltinCodec(codec)) { + string ext = GetFormatExt(QT_TO_UTF8(format)); + is_compatible = FFCodecAndFormatCompatible(codec, ext.c_str()); + } + + QStandardItemModel *model = dynamic_cast(cbox->model()); + QStandardItem *item = model->item(idx); + + if (is_compatible) { + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } else { + if (recEncoder == encName) + currentCompatible = false; + + item->setFlags(Qt::NoItemFlags); + encDisplayName += " "; + encDisplayName += QTStr("CodecCompat.Incompatible").arg(formatName); + } + + item->setText(encDisplayName); + } + + // Set to invalid entry if encoder was incompatible + if (!currentCompatible) + cbox->setCurrentIndex(-1); +} + +void OBSBasicSettings::AdvOutRecCheckCodecs() +{ + QString recFormat = ui->advOutRecFormat->currentData().toString(); + QString recFormatName = ui->advOutRecFormat->currentText(); + + /* Set tooltip if available */ + QString tooltip = QTStr("Basic.Settings.Output.Format.TT." + recFormat.toUtf8()); + + if (!tooltip.startsWith("Basic.Settings.Output")) + ui->advOutRecFormat->setToolTip(tooltip); + else + ui->advOutRecFormat->setToolTip(nullptr); + + QString streamEncoder = ui->advOutEncoder->currentData().toString(); + QString streamAudioEncoder = ui->advOutAEncoder->currentData().toString(); + + int oldVEncoderIdx = ui->advOutRecEncoder->currentIndex(); + int oldAEncoderIdx = ui->advOutRecAEncoder->currentIndex(); + DisableIncompatibleCodecs(ui->advOutRecEncoder, recFormat, recFormatName, streamEncoder); + DisableIncompatibleCodecs(ui->advOutRecAEncoder, recFormat, recFormatName, streamAudioEncoder); + + /* Only invoke AdvOutRecCheckWarnings() if it wouldn't already have + * been triggered by one of the encoder selections being reset. */ + if (ui->advOutRecEncoder->currentIndex() == oldVEncoderIdx && + ui->advOutRecAEncoder->currentIndex() == oldAEncoderIdx) + AdvOutRecCheckWarnings(); +} + +#if defined(__APPLE__) && QT_VERSION < QT_VERSION_CHECK(6, 5, 1) +// Workaround for QTBUG-56064 on macOS +static void ResetInvalidSelection(QComboBox *cbox) +{ + int idx = cbox->currentIndex(); + if (idx < 0) + return; + + QStandardItemModel *model = dynamic_cast(cbox->model()); + QStandardItem *item = model->item(idx); + + if (item->isEnabled()) + return; + + // Reset to "invalid" state if item was disabled + cbox->blockSignals(true); + cbox->setCurrentIndex(-1); + cbox->blockSignals(false); +} +#endif + +void OBSBasicSettings::AdvOutRecCheckWarnings() +{ + auto Checked = [](QCheckBox *box) { + return box->isChecked() ? 1 : 0; + }; + + QString errorMsg; + QString warningMsg; + uint32_t tracks = Checked(ui->advOutRecTrack1) + Checked(ui->advOutRecTrack2) + Checked(ui->advOutRecTrack3) + + Checked(ui->advOutRecTrack4) + Checked(ui->advOutRecTrack5) + Checked(ui->advOutRecTrack6); + + bool useStreamEncoder = ui->advOutRecEncoder->currentIndex() == 0; + if (useStreamEncoder) { + if (!warningMsg.isEmpty()) + warningMsg += "\n\n"; + warningMsg += QTStr("OutputWarnings.CannotPause"); + } + + QString recFormat = ui->advOutRecFormat->currentData().toString(); + + if (recFormat == "flv") { + ui->advRecTrackWidget->setCurrentWidget(ui->flvTracks); + } else { + ui->advRecTrackWidget->setCurrentWidget(ui->recTracks); + + if (tracks == 0) + errorMsg = QTStr("OutputWarnings.NoTracksSelected"); + } + + if (recFormat == "mp4" || recFormat == "mov") { + if (!warningMsg.isEmpty()) + warningMsg += "\n\n"; + + warningMsg += QTStr("OutputWarnings.MP4Recording"); + ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4") + " " + + QTStr("Basic.Settings.Advanced.AutoRemux.MP4")); + } else { + ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4")); + } + +#if defined(__APPLE__) && QT_VERSION < QT_VERSION_CHECK(6, 5, 1) + // Workaround for QTBUG-56064 on macOS + ResetInvalidSelection(ui->advOutRecEncoder); + ResetInvalidSelection(ui->advOutRecAEncoder); +#endif + + // Show warning if codec selection was reset to an invalid state + if (ui->advOutRecEncoder->currentIndex() == -1 || ui->advOutRecAEncoder->currentIndex() == -1) { + if (!warningMsg.isEmpty()) + warningMsg += "\n\n"; + + warningMsg += QTStr("OutputWarnings.CodecIncompatible"); + } + + delete advOutRecWarning; + + if (!errorMsg.isEmpty() || !warningMsg.isEmpty()) { + advOutRecWarning = new QLabel(errorMsg.isEmpty() ? warningMsg : errorMsg, this); + advOutRecWarning->setProperty("class", errorMsg.isEmpty() ? "text-warning" : "text-danger"); + advOutRecWarning->setWordWrap(true); + + ui->advOutRecInfoLayout->addWidget(advOutRecWarning); + } +} + +static inline QString MakeMemorySizeString(int bitrate, int seconds) +{ + QString str = QTStr("Basic.Settings.Advanced.StreamDelay.MemoryUsage"); + int megabytes = bitrate * seconds / 1000 / 8; + + return str.arg(QString::number(megabytes)); +} + +void OBSBasicSettings::UpdateSimpleOutStreamDelayEstimate() +{ + int seconds = ui->streamDelaySec->value(); + int vBitrate = ui->simpleOutputVBitrate->value(); + int aBitrate = ui->simpleOutputABitrate->currentText().toInt(); + + QString msg = MakeMemorySizeString(vBitrate + aBitrate, seconds); + + ui->streamDelayInfo->setText(msg); +} + +void OBSBasicSettings::UpdateAdvOutStreamDelayEstimate() +{ + if (!streamEncoderProps) + return; + + OBSData settings = streamEncoderProps->GetSettings(); + int trackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + QString aBitrateText; + + switch (trackIndex) { + case 1: + aBitrateText = ui->advOutTrack1Bitrate->currentText(); + break; + case 2: + aBitrateText = ui->advOutTrack2Bitrate->currentText(); + break; + case 3: + aBitrateText = ui->advOutTrack3Bitrate->currentText(); + break; + case 4: + aBitrateText = ui->advOutTrack4Bitrate->currentText(); + break; + case 5: + aBitrateText = ui->advOutTrack5Bitrate->currentText(); + break; + case 6: + aBitrateText = ui->advOutTrack6Bitrate->currentText(); + break; + } + + int seconds = ui->streamDelaySec->value(); + int vBitrate = (int)obs_data_get_int(settings, "bitrate"); + int aBitrate = aBitrateText.toInt(); + + QString msg = MakeMemorySizeString(vBitrate + aBitrate, seconds); + + ui->streamDelayInfo->setText(msg); +} + +void OBSBasicSettings::UpdateStreamDelayEstimate() +{ + if (ui->outputMode->currentIndex() == 0) + UpdateSimpleOutStreamDelayEstimate(); + else + UpdateAdvOutStreamDelayEstimate(); + + UpdateAutomaticReplayBufferCheckboxes(); +} + +bool EncoderAvailable(const char *encoder) +{ + const char *val; + int i = 0; + + while (obs_enum_encoder_types(i++, &val)) + if (strcmp(val, encoder) == 0) + return true; + + return false; +} + +void OBSBasicSettings::FillSimpleRecordingValues() +{ +#define ADD_QUALITY(str) \ + ui->simpleOutRecQuality->addItem(QTStr("Basic.Settings.Output.Simple.RecordingQuality." str), QString(str)); +#define ENCODER_STR(str) QTStr("Basic.Settings.Output.Simple.Encoder." str) + + ADD_QUALITY("Stream"); + ADD_QUALITY("Small"); + ADD_QUALITY("HQ"); + ADD_QUALITY("Lossless"); + + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Software"), QString(SIMPLE_ENCODER_X264)); + ui->simpleOutRecEncoder->addItem(ENCODER_STR("SoftwareLowCPU"), QString(SIMPLE_ENCODER_X264_LOWCPU)); + if (EncoderAvailable("obs_qsv11")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.QSV.H264"), QString(SIMPLE_ENCODER_QSV)); + if (EncoderAvailable("obs_qsv11_av1")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.QSV.AV1"), QString(SIMPLE_ENCODER_QSV_AV1)); + if (EncoderAvailable("ffmpeg_nvenc")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.H264"), QString(SIMPLE_ENCODER_NVENC)); + if (EncoderAvailable("obs_nvenc_av1_tex")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.AV1"), QString(SIMPLE_ENCODER_NVENC_AV1)); +#ifdef ENABLE_HEVC + if (EncoderAvailable("h265_texture_amf")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.HEVC"), QString(SIMPLE_ENCODER_AMD_HEVC)); + if (EncoderAvailable("ffmpeg_hevc_nvenc")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.HEVC"), + QString(SIMPLE_ENCODER_NVENC_HEVC)); +#endif + if (EncoderAvailable("h264_texture_amf")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.H264"), QString(SIMPLE_ENCODER_AMD)); + if (EncoderAvailable("av1_texture_amf")) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.AV1"), QString(SIMPLE_ENCODER_AMD_AV1)); + if (EncoderAvailable("com.apple.videotoolbox.videoencoder.ave.avc") +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.Apple.H264"), + QString(SIMPLE_ENCODER_APPLE_H264)); +#ifdef ENABLE_HEVC + if (EncoderAvailable("com.apple.videotoolbox.videoencoder.ave.hevc") +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.Apple.HEVC"), + QString(SIMPLE_ENCODER_APPLE_HEVC)); +#endif + + if (EncoderAvailable("CoreAudio_AAC") || EncoderAvailable("libfdk_aac") || EncoderAvailable("ffmpeg_aac")) + ui->simpleOutRecAEncoder->addItem(QTStr("Basic.Settings.Output.Simple.Codec.AAC.Default"), "aac"); + if (EncoderAvailable("ffmpeg_opus")) + ui->simpleOutRecAEncoder->addItem(QTStr("Basic.Settings.Output.Simple.Codec.Opus"), "opus"); + +#undef ADD_QUALITY +#undef ENCODER_STR +} + +void OBSBasicSettings::FillAudioMonitoringDevices() +{ + QComboBox *cb = ui->monitoringDevice; + + auto enum_devices = [](void *param, const char *name, const char *id) { + QComboBox *cb = (QComboBox *)param; + cb->addItem(name, id); + return true; + }; + + cb->addItem(QTStr("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default"), + "default"); + + obs_enum_audio_monitoring_devices(enum_devices, cb); +} + +void OBSBasicSettings::SimpleRecordingQualityChanged() +{ + QString qual = ui->simpleOutRecQuality->currentData().toString(); + bool streamQuality = qual == "Stream"; + bool losslessQuality = !streamQuality && qual == "Lossless"; + + bool showEncoder = !streamQuality && !losslessQuality; + ui->simpleOutRecEncoder->setVisible(showEncoder); + ui->simpleOutRecEncoderLabel->setVisible(showEncoder); + ui->simpleOutRecAEncoder->setVisible(showEncoder); + ui->simpleOutRecAEncoderLabel->setVisible(showEncoder); + ui->simpleOutRecFormat->setVisible(!losslessQuality); + ui->simpleOutRecFormatLabel->setVisible(!losslessQuality); + + UpdateMultitrackVideo(); + SimpleRecordingEncoderChanged(); + SimpleReplayBufferChanged(); +} + +extern const char *get_simple_output_encoder(const char *encoder); + +void OBSBasicSettings::SimpleStreamingEncoderChanged() +{ + QString encoder = ui->simpleOutStrEncoder->currentData().toString(); + QString preset; + const char *defaultPreset = nullptr; + + ui->simpleOutAdvanced->setVisible(true); + ui->simpleOutPresetLabel->setVisible(true); + ui->simpleOutPreset->setVisible(true); + ui->simpleOutPreset->clear(); + + if (encoder == SIMPLE_ENCODER_QSV || encoder == SIMPLE_ENCODER_QSV_AV1) { + ui->simpleOutPreset->addItem("speed", "speed"); + ui->simpleOutPreset->addItem("balanced", "balanced"); + ui->simpleOutPreset->addItem("quality", "quality"); + + defaultPreset = "balanced"; + preset = curQSVPreset; + + } else if (encoder == SIMPLE_ENCODER_NVENC || encoder == SIMPLE_ENCODER_NVENC_HEVC || + encoder == SIMPLE_ENCODER_NVENC_AV1) { + + const char *name = get_simple_output_encoder(QT_TO_UTF8(encoder)); + const bool isFFmpegEncoder = strncmp(name, "ffmpeg_", 7) == 0; + obs_properties_t *props = obs_get_encoder_properties(name); + + obs_property_t *p = obs_properties_get(props, isFFmpegEncoder ? "preset2" : "preset"); + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *name = obs_property_list_item_name(p, i); + const char *val = obs_property_list_item_string(p, i); + + ui->simpleOutPreset->addItem(QT_UTF8(name), val); + } + + obs_properties_destroy(props); + + defaultPreset = "default"; + preset = curNVENCPreset; + + } else if (encoder == SIMPLE_ENCODER_AMD || encoder == SIMPLE_ENCODER_AMD_HEVC) { + ui->simpleOutPreset->addItem("Speed", "speed"); + ui->simpleOutPreset->addItem("Balanced", "balanced"); + ui->simpleOutPreset->addItem("Quality", "quality"); + + defaultPreset = "balanced"; + preset = curAMDPreset; + } else if (encoder == SIMPLE_ENCODER_APPLE_H264 +#ifdef ENABLE_HEVC + || encoder == SIMPLE_ENCODER_APPLE_HEVC +#endif + ) { + ui->simpleOutAdvanced->setChecked(false); + ui->simpleOutAdvanced->setVisible(false); + ui->simpleOutPreset->setVisible(false); + ui->simpleOutPresetLabel->setVisible(false); + + } else if (encoder == SIMPLE_ENCODER_AMD_AV1) { + ui->simpleOutPreset->addItem("Speed", "speed"); + ui->simpleOutPreset->addItem("Balanced", "balanced"); + ui->simpleOutPreset->addItem("Quality", "quality"); + ui->simpleOutPreset->addItem("High Quality", "highQuality"); + + defaultPreset = "balanced"; + preset = curAMDAV1Preset; + } else { + +#define PRESET_STR(val) QString(Str("Basic.Settings.Output.EncoderPreset." val)).arg(val) + ui->simpleOutPreset->addItem(PRESET_STR("ultrafast"), "ultrafast"); + ui->simpleOutPreset->addItem("superfast", "superfast"); + ui->simpleOutPreset->addItem(PRESET_STR("veryfast"), "veryfast"); + ui->simpleOutPreset->addItem("faster", "faster"); + ui->simpleOutPreset->addItem(PRESET_STR("fast"), "fast"); +#undef PRESET_STR + + /* Users might have previously selected a preset which is no + * longer available in simple mode. Make sure we don't mess + * with their setups without them knowing. */ + if (ui->simpleOutPreset->findData(curPreset) == -1) { + ui->simpleOutPreset->addItem(curPreset, curPreset); + QStandardItemModel *model = qobject_cast(ui->simpleOutPreset->model()); + QStandardItem *item = model->item(model->rowCount() - 1); + item->setEnabled(false); + } + + defaultPreset = "veryfast"; + preset = curPreset; + } + + int idx = ui->simpleOutPreset->findData(QVariant(preset)); + if (idx == -1) + idx = ui->simpleOutPreset->findData(QVariant(defaultPreset)); + + ui->simpleOutPreset->setCurrentIndex(idx); +} + +#define ESTIMATE_STR "Basic.Settings.Output.ReplayBuffer.Estimate" +#define ESTIMATE_TOO_LARGE_STR "Basic.Settings.Output.ReplayBuffer.EstimateTooLarge" +#define ESTIMATE_UNKNOWN_STR "Basic.Settings.Output.ReplayBuffer.EstimateUnknown" + +void OBSBasicSettings::UpdateAutomaticReplayBufferCheckboxes() +{ + bool state = false; + switch (ui->outputMode->currentIndex()) { + case 0: { + const bool lossless = ui->simpleOutRecQuality->currentData().toString() == "Lossless"; + state = ui->simpleReplayBuf->isChecked(); + ui->simpleReplayBuf->setEnabled(!obs_frontend_replay_buffer_active() && !lossless); + break; + } + case 1: { + state = ui->advReplayBuf->isChecked(); + bool customFFmpeg = ui->advOutRecType->currentIndex() == 1; + ui->advReplayBuf->setEnabled(!obs_frontend_replay_buffer_active() && !customFFmpeg); + ui->advReplayBufCustomFFmpeg->setVisible(customFFmpeg); + break; + } + } + ui->replayWhileStreaming->setEnabled(state); + ui->keepReplayStreamStops->setEnabled(state && ui->replayWhileStreaming->isChecked()); +} + +void OBSBasicSettings::SimpleReplayBufferChanged() +{ + QString qual = ui->simpleOutRecQuality->currentData().toString(); + bool streamQuality = qual == "Stream"; + int abitrate = 0; + + ui->simpleRBMegsMax->setVisible(!streamQuality); + ui->simpleRBMegsMaxLabel->setVisible(!streamQuality); + + if (ui->simpleOutRecFormat->currentText().compare("flv") == 0 || streamQuality) { + abitrate = ui->simpleOutputABitrate->currentText().toInt(); + } else { + int delta = ui->simpleOutputABitrate->currentText().toInt(); + if (ui->simpleOutRecTrack1->isChecked()) + abitrate += delta; + if (ui->simpleOutRecTrack2->isChecked()) + abitrate += delta; + if (ui->simpleOutRecTrack3->isChecked()) + abitrate += delta; + if (ui->simpleOutRecTrack4->isChecked()) + abitrate += delta; + if (ui->simpleOutRecTrack5->isChecked()) + abitrate += delta; + if (ui->simpleOutRecTrack6->isChecked()) + abitrate += delta; + } + + int vbitrate = ui->simpleOutputVBitrate->value(); + int seconds = ui->simpleRBSecMax->value(); + + // Set maximum to 75% of installed memory + uint64_t memTotal = os_get_sys_total_size(); + int64_t memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; + + int64_t memMB = int64_t(seconds) * int64_t(vbitrate + abitrate) * 1000 / 8 / 1024 / 1024; + if (memMB < 1) + memMB = 1; + + ui->simpleRBEstimate->setObjectName(""); + if (streamQuality) { + if (memMB <= memMaxMB) { + ui->simpleRBEstimate->setText(QTStr(ESTIMATE_STR).arg(QString::number(int(memMB)))); + } else { + ui->simpleRBEstimate->setText( + QTStr(ESTIMATE_TOO_LARGE_STR) + .arg(QString::number(int(memMB)), QString::number(int(memMaxMB)))); + ui->simpleRBEstimate->setProperty("class", "text-warning"); + } + } else { + ui->simpleRBEstimate->setText(QTStr(ESTIMATE_UNKNOWN_STR)); + ui->simpleRBMegsMax->setMaximum(memMaxMB); + } + + ui->simpleRBEstimate->style()->polish(ui->simpleRBEstimate); + + UpdateAutomaticReplayBufferCheckboxes(); +} + +#define TEXT_USE_STREAM_ENC QTStr("Basic.Settings.Output.Adv.Recording.UseStreamEncoder") + +void OBSBasicSettings::AdvReplayBufferChanged() +{ + obs_data_t *settings; + QString encoder = ui->advOutRecEncoder->currentText(); + bool useStream = QString::compare(encoder, TEXT_USE_STREAM_ENC) == 0; + + if (useStream && streamEncoderProps) { + settings = streamEncoderProps->GetSettings(); + } else if (!useStream && recordEncoderProps) { + settings = recordEncoderProps->GetSettings(); + } else { + if (useStream) + encoder = GetComboData(ui->advOutEncoder); + settings = obs_encoder_defaults(encoder.toUtf8().constData()); + + if (!settings) + return; + + const OBSProfile ¤tProfile = main->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path("recordEncoder.json"); + + if (!jsonFilePath.empty()) { + OBSDataAutoRelease data = + obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + obs_data_apply(settings, data); + } + } + + int vbitrate = (int)obs_data_get_int(settings, "bitrate"); + const char *rateControl = obs_data_get_string(settings, "rate_control"); + + if (!rateControl) + rateControl = ""; + + bool lossless = strcmp(rateControl, "lossless") == 0 || ui->advOutRecType->currentIndex() == 1; + bool replayBufferEnabled = ui->advReplayBuf->isChecked(); + + int abitrate = 0; + if (ui->advOutRecTrack1->isChecked()) + abitrate += ui->advOutTrack1Bitrate->currentText().toInt(); + if (ui->advOutRecTrack2->isChecked()) + abitrate += ui->advOutTrack2Bitrate->currentText().toInt(); + if (ui->advOutRecTrack3->isChecked()) + abitrate += ui->advOutTrack3Bitrate->currentText().toInt(); + if (ui->advOutRecTrack4->isChecked()) + abitrate += ui->advOutTrack4Bitrate->currentText().toInt(); + if (ui->advOutRecTrack5->isChecked()) + abitrate += ui->advOutTrack5Bitrate->currentText().toInt(); + if (ui->advOutRecTrack6->isChecked()) + abitrate += ui->advOutTrack6Bitrate->currentText().toInt(); + + int seconds = ui->advRBSecMax->value(); + + // Set maximum to 75% of installed memory + uint64_t memTotal = os_get_sys_total_size(); + int64_t memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; + + int64_t memMB = int64_t(seconds) * int64_t(vbitrate + abitrate) * 1000 / 8 / 1024 / 1024; + if (memMB < 1) + memMB = 1; + + bool varRateControl = (astrcmpi(rateControl, "CBR") == 0 || astrcmpi(rateControl, "VBR") == 0 || + astrcmpi(rateControl, "ABR") == 0); + if (vbitrate == 0) + varRateControl = false; + + ui->advRBEstimate->setObjectName(""); + if (varRateControl) { + ui->advRBMegsMax->setVisible(false); + ui->advRBMegsMaxLabel->setVisible(false); + + if (memMB <= memMaxMB) { + ui->advRBEstimate->setText(QTStr(ESTIMATE_STR).arg(QString::number(int(memMB)))); + } else { + ui->advRBEstimate->setText( + QTStr(ESTIMATE_TOO_LARGE_STR) + .arg(QString::number(int(memMB)), QString::number(int(memMaxMB)))); + ui->advRBEstimate->setProperty("class", "text-warning"); + } + } else { + ui->advRBMegsMax->setVisible(true); + ui->advRBMegsMaxLabel->setVisible(true); + ui->advRBMegsMax->setMaximum(memMaxMB); + ui->advRBEstimate->setText(QTStr(ESTIMATE_UNKNOWN_STR)); + } + + ui->advReplayBufferFrame->setEnabled(!lossless && replayBufferEnabled); + ui->advRBEstimate->style()->polish(ui->advRBEstimate); + ui->advReplayBuf->setEnabled(!lossless); + + UpdateAutomaticReplayBufferCheckboxes(); +} + +#define SIMPLE_OUTPUT_WARNING(str) QTStr("Basic.Settings.Output.Simple.Warn." str) + +static void DisableIncompatibleSimpleCodecs(QComboBox *cbox, const QString &format) +{ + /* Unlike in advanced mode the available simple mode encoders are + * hardcoded, so this check is also a simpler, hardcoded one. */ + QString encoder = cbox->currentData().toString(); + + bool currentCompatible = true; + for (int idx = 0; idx < cbox->count(); idx++) { + QString encName = cbox->itemData(idx).toString(); + QString codec; + + /* Simple mode does not expose audio encoder variants directly, + * so we have to simply set the codec to the internal name. */ + if (encName == "opus" || encName == "aac") { + codec = encName; + } else { + const char *encoder_id = get_simple_output_encoder(QT_TO_UTF8(encName)); + codec = obs_get_encoder_codec(encoder_id); + } + + QStandardItemModel *model = dynamic_cast(cbox->model()); + QStandardItem *item = model->item(idx); + + if (ContainerSupportsCodec(format.toStdString(), codec.toStdString())) { + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } else { + if (encoder == encName) + currentCompatible = false; + + item->setFlags(Qt::NoItemFlags); + } + } + + if (!currentCompatible) + cbox->setCurrentIndex(-1); +} + +static void DisableIncompatibleSimpleContainer(QComboBox *cbox, const QString ¤tFormat, const QString &vEncoder, + const QString &aEncoder) +{ + /* Similar to above, but works in reverse to disable incompatible formats + * based on the encoder selection. */ + string vCodec = obs_get_encoder_codec(get_simple_output_encoder(QT_TO_UTF8(vEncoder))); + string aCodec = aEncoder.toStdString(); + + bool currentCompatible = true; + for (int idx = 0; idx < cbox->count(); idx++) { + QString format = cbox->itemData(idx).toString(); + string formatStr = format.toStdString(); + + QStandardItemModel *model = dynamic_cast(cbox->model()); + QStandardItem *item = model->item(idx); + + if (ContainerSupportsCodec(formatStr, vCodec) && ContainerSupportsCodec(formatStr, aCodec)) { + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } else { + if (format == currentFormat) + currentCompatible = false; + + item->setFlags(Qt::NoItemFlags); + } + } + + if (!currentCompatible) + cbox->setCurrentIndex(-1); +} + +void OBSBasicSettings::SimpleRecordingEncoderChanged() +{ + QString qual = ui->simpleOutRecQuality->currentData().toString(); + QString warning; + bool enforceBitrate = !ui->ignoreRecommended->isChecked(); + OBSService service = GetStream1Service(); + + delete simpleOutRecWarning; + + if (enforceBitrate && service) { + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + int oldVBitrate = ui->simpleOutputVBitrate->value(); + int oldABitrate = ui->simpleOutputABitrate->currentText().toInt(); + obs_data_set_int(videoSettings, "bitrate", oldVBitrate); + obs_data_set_int(audioSettings, "bitrate", oldABitrate); + + obs_service_apply_encoder_settings(service, videoSettings, audioSettings); + + int newVBitrate = obs_data_get_int(videoSettings, "bitrate"); + int newABitrate = obs_data_get_int(audioSettings, "bitrate"); + + if (newVBitrate < oldVBitrate) + warning = SIMPLE_OUTPUT_WARNING("VideoBitrate").arg(newVBitrate); + if (newABitrate < oldABitrate) { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("AudioBitrate").arg(newABitrate); + } + } + + QString format = ui->simpleOutRecFormat->currentData().toString(); + /* Set tooltip if available */ + QString tooltip = QTStr("Basic.Settings.Output.Format.TT." + format.toUtf8()); + + if (!tooltip.startsWith("Basic.Settings.Output")) + ui->simpleOutRecFormat->setToolTip(tooltip); + else + ui->simpleOutRecFormat->setToolTip(nullptr); + + if (qual == "Lossless") { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("Lossless"); + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("Encoder"); + + } else if (qual != "Stream") { + QString enc = ui->simpleOutRecEncoder->currentData().toString(); + QString streamEnc = ui->simpleOutStrEncoder->currentData().toString(); + bool x264RecEnc = (enc == SIMPLE_ENCODER_X264 || enc == SIMPLE_ENCODER_X264_LOWCPU); + + if (streamEnc == SIMPLE_ENCODER_X264 && x264RecEnc) { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("Encoder"); + } + + /* Prevent function being called recursively if changes happen. */ + ui->simpleOutRecEncoder->blockSignals(true); + ui->simpleOutRecAEncoder->blockSignals(true); + DisableIncompatibleSimpleCodecs(ui->simpleOutRecEncoder, format); + DisableIncompatibleSimpleCodecs(ui->simpleOutRecAEncoder, format); + ui->simpleOutRecAEncoder->blockSignals(false); + ui->simpleOutRecEncoder->blockSignals(false); + + if (ui->simpleOutRecEncoder->currentIndex() == -1 || ui->simpleOutRecAEncoder->currentIndex() == -1) { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += QTStr("OutputWarnings.CodecIncompatible"); + } + } else { + /* When using stream encoders do the reverse; Disable containers that are incompatible. */ + QString streamEnc = ui->simpleOutStrEncoder->currentData().toString(); + QString streamAEnc = ui->simpleOutStrAEncoder->currentData().toString(); + + ui->simpleOutRecFormat->blockSignals(true); + DisableIncompatibleSimpleContainer(ui->simpleOutRecFormat, format, streamEnc, streamAEnc); + ui->simpleOutRecFormat->blockSignals(false); + + if (ui->simpleOutRecFormat->currentIndex() == -1) { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("IncompatibleContainer"); + } + + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("CannotPause"); + } + + if (qual != "Lossless" && (format == "mp4" || format == "mov")) { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += QTStr("OutputWarnings.MP4Recording"); + ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4") + " " + + QTStr("Basic.Settings.Advanced.AutoRemux.MP4")); + } else { + ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4")); + } + + if (qual == "Stream") { + ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleFlvTracks); + } else if (qual == "Lossless") { + ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleRecTracks); + } else { + if (format == "flv") { + ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleFlvTracks); + } else { + ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleRecTracks); + } + } + + if (warning.isEmpty()) + return; + + simpleOutRecWarning = new QLabel(warning, this); + simpleOutRecWarning->setProperty("class", "text-warning"); + simpleOutRecWarning->setWordWrap(true); + ui->simpleOutInfoLayout->addWidget(simpleOutRecWarning); +} + +void OBSBasicSettings::SurroundWarning(int idx) +{ + if (idx == lastChannelSetupIdx || idx == -1) + return; + + if (loading) { + lastChannelSetupIdx = idx; + return; + } + + QString speakerLayoutQstr = ui->channelSetup->itemText(idx); + bool surround = IsSurround(QT_TO_UTF8(speakerLayoutQstr)); + + QString lastQstr = ui->channelSetup->itemText(lastChannelSetupIdx); + bool wasSurround = IsSurround(QT_TO_UTF8(lastQstr)); + + if (surround && !wasSurround) { + QMessageBox::StandardButton button; + + QString warningString = QTStr("Basic.Settings.ProgramRestart") + QStringLiteral("\n\n") + + QTStr(MULTI_CHANNEL_WARNING) + QStringLiteral("\n\n") + + QTStr(MULTI_CHANNEL_WARNING ".Confirm"); + + button = OBSMessageBox::question(this, QTStr(MULTI_CHANNEL_WARNING ".Title"), warningString); + + if (button == QMessageBox::No) { + QMetaObject::invokeMethod(ui->channelSetup, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(int, lastChannelSetupIdx)); + return; + } + } + + lastChannelSetupIdx = idx; +} + +#define LL_BUFFERING_WARNING "Basic.Settings.Audio.LowLatencyBufferingWarning" + +void OBSBasicSettings::UpdateAudioWarnings() +{ + QString speakerLayoutQstr = ui->channelSetup->currentText(); + bool surround = IsSurround(QT_TO_UTF8(speakerLayoutQstr)); + bool lowBufferingActive = ui->lowLatencyBuffering->isChecked(); + + QString text; + + if (surround) { + text = QTStr(MULTI_CHANNEL_WARNING ".Enabled") + QStringLiteral("\n\n") + QTStr(MULTI_CHANNEL_WARNING); + } + + if (lowBufferingActive) { + if (!text.isEmpty()) + text += QStringLiteral("\n\n"); + + text += QTStr(LL_BUFFERING_WARNING ".Enabled") + QStringLiteral("\n\n") + QTStr(LL_BUFFERING_WARNING); + } + + ui->audioMsg_2->setText(text); + ui->audioMsg_2->setVisible(!text.isEmpty()); +} + +void OBSBasicSettings::LowLatencyBufferingChanged(bool checked) +{ + if (checked) { + QString warningStr = + QTStr(LL_BUFFERING_WARNING) + QStringLiteral("\n\n") + QTStr(LL_BUFFERING_WARNING ".Confirm"); + + auto button = OBSMessageBox::question(this, QTStr(LL_BUFFERING_WARNING ".Title"), warningStr); + + if (button == QMessageBox::No) { + QMetaObject::invokeMethod(ui->lowLatencyBuffering, "setChecked", Qt::QueuedConnection, + Q_ARG(bool, false)); + return; + } + } + + QMetaObject::invokeMethod(this, "UpdateAudioWarnings", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "AudioChangedRestart"); +} + +void OBSBasicSettings::SimpleRecordingQualityLosslessWarning(int idx) +{ + if (idx == lastSimpleRecQualityIdx || idx == -1) + return; + + QString qual = ui->simpleOutRecQuality->itemData(idx).toString(); + + if (loading) { + lastSimpleRecQualityIdx = idx; + return; + } + + if (qual == "Lossless") { + QMessageBox::StandardButton button; + + QString warningString = + SIMPLE_OUTPUT_WARNING("Lossless") + QString("\n\n") + SIMPLE_OUTPUT_WARNING("Lossless.Msg"); + + button = OBSMessageBox::question(this, SIMPLE_OUTPUT_WARNING("Lossless.Title"), warningString); + + if (button == QMessageBox::No) { + QMetaObject::invokeMethod(ui->simpleOutRecQuality, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(int, lastSimpleRecQualityIdx)); + return; + } + } + + lastSimpleRecQualityIdx = idx; +} + +void OBSBasicSettings::on_disableOSXVSync_clicked() +{ +#ifdef __APPLE__ + if (!loading) { + bool disable = ui->disableOSXVSync->isChecked(); + ui->resetOSXVSync->setEnabled(disable); + } +#endif +} + +QIcon OBSBasicSettings::GetGeneralIcon() const +{ + return generalIcon; +} + +QIcon OBSBasicSettings::GetAppearanceIcon() const +{ + return appearanceIcon; +} + +QIcon OBSBasicSettings::GetStreamIcon() const +{ + return streamIcon; +} + +QIcon OBSBasicSettings::GetOutputIcon() const +{ + return outputIcon; +} + +QIcon OBSBasicSettings::GetAudioIcon() const +{ + return audioIcon; +} + +QIcon OBSBasicSettings::GetVideoIcon() const +{ + return videoIcon; +} + +QIcon OBSBasicSettings::GetHotkeysIcon() const +{ + return hotkeysIcon; +} + +QIcon OBSBasicSettings::GetAccessibilityIcon() const +{ + return accessibilityIcon; +} + +QIcon OBSBasicSettings::GetAdvancedIcon() const +{ + return advancedIcon; +} + +void OBSBasicSettings::SetGeneralIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::GENERAL)->setIcon(icon); +} + +void OBSBasicSettings::SetAppearanceIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::APPEARANCE)->setIcon(icon); +} + +void OBSBasicSettings::SetStreamIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::STREAM)->setIcon(icon); +} + +void OBSBasicSettings::SetOutputIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::OUTPUT)->setIcon(icon); +} + +void OBSBasicSettings::SetAudioIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::AUDIO)->setIcon(icon); +} + +void OBSBasicSettings::SetVideoIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::VIDEO)->setIcon(icon); +} + +void OBSBasicSettings::SetHotkeysIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::HOTKEYS)->setIcon(icon); +} + +void OBSBasicSettings::SetAccessibilityIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::ACCESSIBILITY)->setIcon(icon); +} + +void OBSBasicSettings::SetAdvancedIcon(const QIcon &icon) +{ + ui->listWidget->item(Pages::ADVANCED)->setIcon(icon); +} + +int OBSBasicSettings::CurrentFLVTrack() +{ + if (ui->flvTrack1->isChecked()) + return 1; + else if (ui->flvTrack2->isChecked()) + return 2; + else if (ui->flvTrack3->isChecked()) + return 3; + else if (ui->flvTrack4->isChecked()) + return 4; + else if (ui->flvTrack5->isChecked()) + return 5; + else if (ui->flvTrack6->isChecked()) + return 6; + + return 0; +} + +int OBSBasicSettings::SimpleOutGetSelectedAudioTracks() +{ + int tracks = (ui->simpleOutRecTrack1->isChecked() ? (1 << 0) : 0) | + (ui->simpleOutRecTrack2->isChecked() ? (1 << 1) : 0) | + (ui->simpleOutRecTrack3->isChecked() ? (1 << 2) : 0) | + (ui->simpleOutRecTrack4->isChecked() ? (1 << 3) : 0) | + (ui->simpleOutRecTrack5->isChecked() ? (1 << 4) : 0) | + (ui->simpleOutRecTrack6->isChecked() ? (1 << 5) : 0); + return tracks; +} + +int OBSBasicSettings::AdvOutGetSelectedAudioTracks() +{ + int tracks = + (ui->advOutRecTrack1->isChecked() ? (1 << 0) : 0) | (ui->advOutRecTrack2->isChecked() ? (1 << 1) : 0) | + (ui->advOutRecTrack3->isChecked() ? (1 << 2) : 0) | (ui->advOutRecTrack4->isChecked() ? (1 << 3) : 0) | + (ui->advOutRecTrack5->isChecked() ? (1 << 4) : 0) | (ui->advOutRecTrack6->isChecked() ? (1 << 5) : 0); + return tracks; +} + +int OBSBasicSettings::AdvOutGetStreamingSelectedAudioTracks() +{ + int tracks = (ui->advOutMultiTrack1->isChecked() ? (1 << 0) : 0) | + (ui->advOutMultiTrack2->isChecked() ? (1 << 1) : 0) | + (ui->advOutMultiTrack3->isChecked() ? (1 << 2) : 0) | + (ui->advOutMultiTrack4->isChecked() ? (1 << 3) : 0) | + (ui->advOutMultiTrack5->isChecked() ? (1 << 4) : 0) | + (ui->advOutMultiTrack6->isChecked() ? (1 << 5) : 0); + return tracks; +} + +/* Using setEditable(true) on a QComboBox when there's a custom style in use + * does not work properly, so instead completely recreate the widget, which + * seems to work fine. */ +void OBSBasicSettings::RecreateOutputResolutionWidget() +{ + QSizePolicy sizePolicy = ui->outputResolution->sizePolicy(); + bool changed = WidgetChanged(ui->outputResolution); + + delete ui->outputResolution; + ui->outputResolution = new QComboBox(ui->videoPage); + ui->outputResolution->setObjectName(QString::fromUtf8("outputResolution")); + ui->outputResolution->setSizePolicy(sizePolicy); + ui->outputResolution->setEditable(true); + ui->outputResolution->setProperty("changed", changed); + ui->outputResLabel->setBuddy(ui->outputResolution); + + ui->outputResLayout->insertWidget(0, ui->outputResolution); + + QWidget::setTabOrder(ui->baseResolution, ui->outputResolution); + QWidget::setTabOrder(ui->outputResolution, ui->downscaleFilter); + + HookWidget(ui->outputResolution, CBEDIT_CHANGED, VIDEO_RES); + + connect(ui->outputResolution, &QComboBox::editTextChanged, this, + &OBSBasicSettings::on_outputResolution_editTextChanged); + + ui->outputResolution->lineEdit()->setValidator(ui->baseResolution->lineEdit()->validator()); +} + +void OBSBasicSettings::UpdateAdvNetworkGroup() +{ + bool enabled = protocol.contains("RTMP"); + + ui->advNetworkDisabled->setVisible(!enabled); + + ui->bindToIPLabel->setVisible(enabled); + ui->bindToIP->setVisible(enabled); + ui->dynBitrate->setVisible(enabled); + ui->ipFamilyLabel->setVisible(enabled); + ui->ipFamily->setVisible(enabled); +#ifdef _WIN32 + ui->enableNewSocketLoop->setVisible(enabled); + ui->enableLowLatencyMode->setVisible(enabled); +#endif +} + +extern bool MultitrackVideoDeveloperModeEnabled(); + +void OBSBasicSettings::UpdateMultitrackVideo() +{ + // Technically, it should currently be safe to toggle multitrackVideo + // while not streaming (recording should be irrelevant), but practically + // output settings aren't currently being tracked with that degree of + // flexibility, so just disable everything while outputs are active. + auto toggle_available = !main->Active(); + + // FIXME: protocol is not updated properly for WHIP; what do? + auto available = protocol.startsWith("RTMP"); + + if (available && !IsCustomService()) { + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); + OBSServiceAutoRelease temp_service = + obs_service_create_private("rtmp_common", "auto config query service", settings); + settings = obs_service_get_settings(temp_service); + available = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + if (!available && ui->enableMultitrackVideo->isChecked()) + ui->enableMultitrackVideo->setChecked(false); + } + +#ifndef _WIN32 + available = available && MultitrackVideoDeveloperModeEnabled(); +#endif + + if (IsCustomService()) + available = available && MultitrackVideoDeveloperModeEnabled(); + + ui->multitrackVideoGroupBox->setVisible(available); + + ui->enableMultitrackVideo->setEnabled(toggle_available); + + ui->multitrackVideoMaximumAggregateBitrateLabel->setEnabled(toggle_available && + ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumAggregateBitrateAuto->setEnabled(toggle_available && + ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumAggregateBitrate->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked() && + !ui->multitrackVideoMaximumAggregateBitrateAuto->isChecked()); + + ui->multitrackVideoMaximumVideoTracksLabel->setEnabled(toggle_available && + ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumVideoTracksAuto->setEnabled(toggle_available && + ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumVideoTracks->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && + !ui->multitrackVideoMaximumVideoTracksAuto->isChecked()); + + ui->multitrackVideoStreamDumpEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverrideEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverrideLabel->setVisible(available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverride->setVisible(available && MultitrackVideoDeveloperModeEnabled()); + + ui->multitrackVideoStreamDumpEnable->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoConfigOverrideEnable->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoConfigOverrideLabel->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && + ui->multitrackVideoConfigOverrideEnable->isChecked()); + ui->multitrackVideoConfigOverride->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && + ui->multitrackVideoConfigOverrideEnable->isChecked()); + + auto update_simple_output_settings = [&](bool mtv_enabled) { + auto recording_uses_stream_encoder = ui->simpleOutRecQuality->currentData().toString() == "Stream"; + mtv_enabled = mtv_enabled && !recording_uses_stream_encoder; + + ui->simpleOutputVBitrateLabel->setDisabled(mtv_enabled); + ui->simpleOutputVBitrate->setDisabled(mtv_enabled); + + ui->simpleOutputABitrateLabel->setDisabled(mtv_enabled); + ui->simpleOutputABitrate->setDisabled(mtv_enabled); + + ui->simpleOutStrEncoderLabel->setDisabled(mtv_enabled); + ui->simpleOutStrEncoder->setDisabled(mtv_enabled); + + ui->simpleOutPresetLabel->setDisabled(mtv_enabled); + ui->simpleOutPreset->setDisabled(mtv_enabled); + + ui->simpleOutCustomLabel->setDisabled(mtv_enabled); + ui->simpleOutCustom->setDisabled(mtv_enabled); + + ui->simpleOutStrAEncoderLabel->setDisabled(mtv_enabled); + ui->simpleOutStrAEncoder->setDisabled(mtv_enabled); + }; + + auto update_advanced_output_settings = [&](bool mtv_enabled) { + auto recording_uses_stream_video_encoder = ui->advOutRecEncoder->currentText() == TEXT_USE_STREAM_ENC; + auto recording_uses_stream_audio_encoder = ui->advOutRecAEncoder->currentData() == "none"; + auto disable_video = mtv_enabled && !recording_uses_stream_video_encoder; + auto disable_audio = mtv_enabled && !recording_uses_stream_audio_encoder; + + ui->advOutAEncLabel->setDisabled(disable_audio); + ui->advOutAEncoder->setDisabled(disable_audio); + + ui->advOutEncLabel->setDisabled(disable_video); + ui->advOutEncoder->setDisabled(disable_video); + + ui->advOutUseRescale->setDisabled(disable_video); + ui->advOutRescale->setDisabled(disable_video); + ui->advOutRescaleFilter->setDisabled(disable_video); + + if (streamEncoderProps) + streamEncoderProps->SetDisabled(disable_video); + }; + + auto update_advanced_output_audio_tracks = [&](bool mtv_enabled) { + auto vod_track_enabled = vodTrackCheckbox && vodTrackCheckbox->isChecked(); + + auto vod_track_idx_enabled = [&](size_t idx) { + return vod_track_enabled && vodTrack[idx - 1] && vodTrack[idx - 1]->isChecked(); + }; + + auto track1_warning_visible = mtv_enabled && + (ui->advOutTrack1->isChecked() || vod_track_idx_enabled(1)); + auto track1_disabled = track1_warning_visible && !ui->advOutRecTrack1->isChecked(); + ui->advOutTrack1BitrateLabel->setDisabled(track1_disabled); + ui->advOutTrack1Bitrate->setDisabled(track1_disabled); + + auto track2_warning_visible = mtv_enabled && + (ui->advOutTrack2->isChecked() || vod_track_idx_enabled(2)); + auto track2_disabled = track2_warning_visible && !ui->advOutRecTrack2->isChecked(); + ui->advOutTrack2BitrateLabel->setDisabled(track2_disabled); + ui->advOutTrack2Bitrate->setDisabled(track2_disabled); + + auto track3_warning_visible = mtv_enabled && + (ui->advOutTrack3->isChecked() || vod_track_idx_enabled(3)); + auto track3_disabled = track3_warning_visible && !ui->advOutRecTrack3->isChecked(); + ui->advOutTrack3BitrateLabel->setDisabled(track3_disabled); + ui->advOutTrack3Bitrate->setDisabled(track3_disabled); + + auto track4_warning_visible = mtv_enabled && + (ui->advOutTrack4->isChecked() || vod_track_idx_enabled(4)); + auto track4_disabled = track4_warning_visible && !ui->advOutRecTrack4->isChecked(); + ui->advOutTrack4BitrateLabel->setDisabled(track4_disabled); + ui->advOutTrack4Bitrate->setDisabled(track4_disabled); + + auto track5_warning_visible = mtv_enabled && + (ui->advOutTrack5->isChecked() || vod_track_idx_enabled(5)); + auto track5_disabled = track5_warning_visible && !ui->advOutRecTrack5->isChecked(); + ui->advOutTrack5BitrateLabel->setDisabled(track5_disabled); + ui->advOutTrack5Bitrate->setDisabled(track5_disabled); + + auto track6_warning_visible = mtv_enabled && + (ui->advOutTrack6->isChecked() || vod_track_idx_enabled(6)); + auto track6_disabled = track6_warning_visible && !ui->advOutRecTrack6->isChecked(); + ui->advOutTrack6BitrateLabel->setDisabled(track6_disabled); + ui->advOutTrack6Bitrate->setDisabled(track6_disabled); + }; + + if (available) { + OBSDataAutoRelease settings; + { + auto service_name = ui->service->currentText(); + auto custom_server = ui->customServer->text().trimmed(); + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *service = obs_properties_get(props, "service"); + + settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(service_name)); + obs_property_modified(service, settings); + + obs_properties_destroy(props); + } + + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(settings, "multitrack_video_name")) + multitrack_video_name = obs_data_get_string(settings, "multitrack_video_name"); + + ui->enableMultitrackVideo->setText( + QTStr("Basic.Settings.Stream.EnableMultitrackVideo").arg(multitrack_video_name)); + + if (obs_data_has_user_value(settings, "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setVisible(true); + ui->multitrackVideoInfo->setText(obs_data_get_string(settings, "multitrack_video_disclaimer")); + } else { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info").arg(multitrack_video_name, ui->service->currentText())); + } + + auto disabled_text = QTStr("Basic.Settings.MultitrackVideoDisabledSettings") + .arg(ui->service->currentText()) + .arg(multitrack_video_name); + + ui->multitrackVideoNotice->setText(disabled_text); + + auto mtv_enabled = ui->enableMultitrackVideo->isChecked(); + ui->multitrackVideoNoticeBox->setVisible(mtv_enabled); + + update_simple_output_settings(mtv_enabled); + update_advanced_output_settings(mtv_enabled); + update_advanced_output_audio_tracks(mtv_enabled); + } else { + ui->multitrackVideoNoticeBox->setVisible(false); + + update_simple_output_settings(false); + update_advanced_output_settings(false); + update_advanced_output_audio_tracks(false); + } +} + +void OBSBasicSettings::SimpleStreamAudioEncoderChanged() +{ + PopulateSimpleBitrates(ui->simpleOutputABitrate, ui->simpleOutStrAEncoder->currentData().toString() == "opus"); + + if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) + return; + + RestrictResetBitrates({ui->simpleOutputABitrate}, 320); +} + +void OBSBasicSettings::AdvAudioEncodersChanged() +{ + QString streamEncoder = ui->advOutAEncoder->currentData().toString(); + QString recEncoder = ui->advOutRecAEncoder->currentData().toString(); + + if (recEncoder == "none") + recEncoder = streamEncoder; + + PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, + ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + QT_TO_UTF8(streamEncoder), QT_TO_UTF8(recEncoder)); + + if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) + return; + + RestrictResetBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, + ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + 320); +} From 00fc9035a4c349f395ea41f61b7bd7c82b2aaeeb Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 20:15:10 +0100 Subject: [PATCH 15/37] frontend: Add renamed Qt UI dialogs --- .../dialogs/NameDialog.cpp | 12 +++++-- .../dialogs/NameDialog.hpp | 11 +++---- .../dialogs/OBSAbout.cpp | 16 +++++---- .../dialogs/OBSAbout.hpp | 5 ++- .../dialogs/OBSBasicAdvAudio.cpp | 14 ++++---- .../dialogs/OBSBasicAdvAudio.hpp | 3 +- .../dialogs/OBSBasicFilters.cpp | 33 ++++++++----------- .../dialogs/OBSBasicFilters.hpp | 10 ++---- .../dialogs/OBSBasicProperties.cpp | 23 ++++++------- .../dialogs/OBSBasicProperties.hpp | 15 +++------ .../dialogs/OBSBasicSourceSelect.cpp | 8 ++--- .../dialogs/OBSBasicSourceSelect.hpp | 12 +++---- .../dialogs/OBSBasicTransform.cpp | 9 +++-- .../dialogs/OBSBasicTransform.hpp | 7 ++-- .../dialogs/OBSBasicVCamConfig.cpp | 9 ++--- .../dialogs/OBSBasicVCamConfig.hpp | 10 ++---- .../dialogs/OBSLogReply.cpp | 11 ++++--- .../dialogs/OBSLogReply.hpp | 3 +- .../dialogs/OBSLogViewer.cpp | 20 +++++------ .../dialogs/OBSLogViewer.hpp | 6 ++-- .../dialogs/OBSPermissions.cpp | 8 +++-- .../dialogs/OBSPermissions.hpp | 5 ++- .../dialogs/OBSUpdate.cpp | 7 ++-- .../dialogs/OBSUpdate.hpp | 3 +- .../dialogs/OBSWhatsNew.cpp | 14 ++++---- .../dialogs/OBSWhatsNew.hpp | 1 + .../dialogs/OBSYoutubeActions.cpp | 15 +++++---- .../dialogs/OBSYoutubeActions.hpp | 9 +++-- 28 files changed, 143 insertions(+), 156 deletions(-) rename UI/window-namedialog.cpp => frontend/dialogs/NameDialog.cpp (95%) rename UI/window-namedialog.hpp => frontend/dialogs/NameDialog.hpp (93%) rename UI/window-basic-about.cpp => frontend/dialogs/OBSAbout.cpp (96%) rename UI/window-basic-about.hpp => frontend/dialogs/OBSAbout.hpp (93%) rename UI/window-basic-adv-audio.cpp => frontend/dialogs/OBSBasicAdvAudio.cpp (95%) rename UI/window-basic-adv-audio.hpp => frontend/dialogs/OBSBasicAdvAudio.hpp (96%) rename UI/window-basic-filters.cpp => frontend/dialogs/OBSBasicFilters.cpp (98%) rename UI/window-basic-filters.hpp => frontend/dialogs/OBSBasicFilters.hpp (97%) rename UI/window-basic-properties.cpp => frontend/dialogs/OBSBasicProperties.cpp (97%) rename UI/window-basic-properties.hpp => frontend/dialogs/OBSBasicProperties.hpp (95%) rename UI/window-basic-source-select.cpp => frontend/dialogs/OBSBasicSourceSelect.cpp (99%) rename UI/window-basic-source-select.hpp => frontend/dialogs/OBSBasicSourceSelect.hpp (94%) rename UI/window-basic-transform.cpp => frontend/dialogs/OBSBasicTransform.cpp (98%) rename UI/window-basic-transform.hpp => frontend/dialogs/OBSBasicTransform.hpp (98%) rename UI/window-basic-vcam-config.cpp => frontend/dialogs/OBSBasicVCamConfig.cpp (95%) rename UI/window-basic-vcam-config.hpp => frontend/dialogs/OBSBasicVCamConfig.hpp (85%) rename UI/window-log-reply.cpp => frontend/dialogs/OBSLogReply.cpp (95%) rename UI/window-log-reply.hpp => frontend/dialogs/OBSLogReply.hpp (98%) rename UI/log-viewer.cpp => frontend/dialogs/OBSLogViewer.cpp (94%) rename UI/log-viewer.hpp => frontend/dialogs/OBSLogViewer.hpp (88%) rename UI/window-permissions.cpp => frontend/dialogs/OBSPermissions.cpp (97%) rename UI/window-permissions.hpp => frontend/dialogs/OBSPermissions.hpp (96%) rename UI/update/update-window.cpp => frontend/dialogs/OBSUpdate.cpp (89%) rename UI/update/update-window.hpp => frontend/dialogs/OBSUpdate.hpp (89%) rename UI/window-whats-new.cpp => frontend/dialogs/OBSWhatsNew.cpp (90%) rename UI/window-whats-new.hpp => frontend/dialogs/OBSWhatsNew.hpp (99%) rename UI/window-youtube-actions.cpp => frontend/dialogs/OBSYoutubeActions.cpp (99%) rename UI/window-youtube-actions.hpp => frontend/dialogs/OBSYoutubeActions.hpp (96%) diff --git a/UI/window-namedialog.cpp b/frontend/dialogs/NameDialog.cpp similarity index 95% rename from UI/window-namedialog.cpp rename to frontend/dialogs/NameDialog.cpp index 561bd15b9..5458fafb6 100644 --- a/UI/window-namedialog.cpp +++ b/frontend/dialogs/NameDialog.cpp @@ -15,12 +15,18 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-namedialog.cpp" -#include "obs-app.hpp" +#include "NameDialog.hpp" -#include +#include + +#include +#include +#include +#include #include +#include "moc_NameDialog.cpp" + NameDialog::NameDialog(QWidget *parent) : QDialog(parent) { installEventFilter(CreateShortcutFilter()); diff --git a/UI/window-namedialog.hpp b/frontend/dialogs/NameDialog.hpp similarity index 93% rename from UI/window-namedialog.hpp rename to frontend/dialogs/NameDialog.hpp index e0f7006ba..a61f3dfd3 100644 --- a/UI/window-namedialog.hpp +++ b/frontend/dialogs/NameDialog.hpp @@ -18,12 +18,11 @@ #pragma once #include -#include -#include -#include -#include -#include -#include + +class QCheckBox; +class QLabel; +class QLineEdit; +class QString; class NameDialog : public QDialog { Q_OBJECT diff --git a/UI/window-basic-about.cpp b/frontend/dialogs/OBSAbout.cpp similarity index 96% rename from UI/window-basic-about.cpp rename to frontend/dialogs/OBSAbout.cpp index 3509e279b..e9103dbab 100644 --- a/UI/window-basic-about.cpp +++ b/frontend/dialogs/OBSAbout.cpp @@ -1,14 +1,18 @@ -#include "moc_window-basic-about.cpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" +#include "OBSAbout.hpp" + +#include +#include + #include -#include -#include -#include + #include +#include "moc_OBSAbout.cpp" + using namespace json11; +extern bool steam; + OBSAbout::OBSAbout(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAbout) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/UI/window-basic-about.hpp b/frontend/dialogs/OBSAbout.hpp similarity index 93% rename from UI/window-basic-about.hpp rename to frontend/dialogs/OBSAbout.hpp index bb507fe2c..e3ec87c36 100644 --- a/UI/window-basic-about.hpp +++ b/frontend/dialogs/OBSAbout.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include "ui_OBSAbout.h" +#include + class OBSAbout : public QDialog { Q_OBJECT diff --git a/UI/window-basic-adv-audio.cpp b/frontend/dialogs/OBSBasicAdvAudio.cpp similarity index 95% rename from UI/window-basic-adv-audio.cpp rename to frontend/dialogs/OBSBasicAdvAudio.cpp index 001b804d7..8992e9822 100644 --- a/UI/window-basic-adv-audio.cpp +++ b/frontend/dialogs/OBSBasicAdvAudio.cpp @@ -1,13 +1,11 @@ -#include "window-basic-adv-audio.hpp" -#include "window-basic-main.hpp" -#include "item-widget-helpers.hpp" -#include "adv-audio-control.hpp" -#include "obs-app.hpp" -#include - +#include "OBSBasicAdvAudio.hpp" #include "ui_OBSAdvAudio.h" -Q_DECLARE_METATYPE(OBSSource); +#include +#include +#include + +#include "moc_OBSBasicAdvAudio.cpp" OBSBasicAdvAudio::OBSBasicAdvAudio(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAdvAudio), showInactive(false) { diff --git a/UI/window-basic-adv-audio.hpp b/frontend/dialogs/OBSBasicAdvAudio.hpp similarity index 96% rename from UI/window-basic-adv-audio.hpp rename to frontend/dialogs/OBSBasicAdvAudio.hpp index 28b6fa05b..98cb61b0a 100644 --- a/UI/window-basic-adv-audio.hpp +++ b/frontend/dialogs/OBSBasicAdvAudio.hpp @@ -1,9 +1,8 @@ #pragma once #include + #include -#include -#include class OBSAdvAudioCtrl; class Ui_OBSAdvAudio; diff --git a/UI/window-basic-filters.cpp b/frontend/dialogs/OBSBasicFilters.cpp similarity index 98% rename from UI/window-basic-filters.cpp rename to frontend/dialogs/OBSBasicFilters.cpp index c92036c89..7567757ee 100644 --- a/UI/window-basic-filters.cpp +++ b/frontend/dialogs/OBSBasicFilters.cpp @@ -15,35 +15,28 @@ along with this program. If not, see . ******************************************************************************/ -#include "properties-view.hpp" -#include "window-namedialog.hpp" -#include "window-basic-main.hpp" -#include "window-basic-filters.hpp" -#include "display-helpers.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "obs-app.hpp" -#include "undo-stack-obs.hpp" +#include "OBSBasicFilters.hpp" +#include +#include +#include +#include +#include +#include + +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif -using namespace std; +#include "moc_OBSBasicFilters.cpp" -Q_DECLARE_METATYPE(OBSSource); +using namespace std; OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_) : QDialog(parent), diff --git a/UI/window-basic-filters.hpp b/frontend/dialogs/OBSBasicFilters.hpp similarity index 97% rename from UI/window-basic-filters.hpp rename to frontend/dialogs/OBSBasicFilters.hpp index c1e2e6c98..a02d51914 100644 --- a/UI/window-basic-filters.hpp +++ b/frontend/dialogs/OBSBasicFilters.hpp @@ -17,16 +17,12 @@ #pragma once +#include "ui_OBSBasicFilters.h" + #include -#include -#include -#include -#include class OBSBasic; -class QMenu; - -#include "ui_OBSBasicFilters.h" +class OBSPropertiesView; class OBSBasicFilters : public QDialog { Q_OBJECT diff --git a/UI/window-basic-properties.cpp b/frontend/dialogs/OBSBasicProperties.cpp similarity index 97% rename from UI/window-basic-properties.cpp rename to frontend/dialogs/OBSBasicProperties.cpp index 342b02dd4..ee656f595 100644 --- a/UI/window-basic-properties.cpp +++ b/frontend/dialogs/OBSBasicProperties.cpp @@ -15,27 +15,24 @@ along with this program. If not, see . ******************************************************************************/ -#include "obs-app.hpp" -#include "moc_window-basic-properties.cpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" +#include "OBSBasicProperties.hpp" + +#include +#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif +#include "moc_OBSBasicProperties.cpp" + using namespace std; static void CreateTransitionScene(OBSSource scene, const char *text, uint32_t color); diff --git a/UI/window-basic-properties.hpp b/frontend/dialogs/OBSBasicProperties.hpp similarity index 95% rename from UI/window-basic-properties.hpp rename to frontend/dialogs/OBSBasicProperties.hpp index 3f9557c2c..91738115c 100644 --- a/UI/window-basic-properties.hpp +++ b/frontend/dialogs/OBSBasicProperties.hpp @@ -17,18 +17,13 @@ #pragma once -#include -#include -#include -#include -#include "qt-display.hpp" -#include - -class OBSPropertiesView; -class OBSBasic; - #include "ui_OBSBasicProperties.h" +#include + +class OBSBasic; +class OBSPropertiesView; + class OBSBasicProperties : public QDialog { Q_OBJECT diff --git a/UI/window-basic-source-select.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp similarity index 99% rename from UI/window-basic-source-select.cpp rename to frontend/dialogs/OBSBasicSourceSelect.cpp index 430a3f140..84e9ad44e 100644 --- a/UI/window-basic-source-select.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -15,11 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "OBSBasicSourceSelect.hpp" + #include -#include "window-basic-main.hpp" -#include "moc_window-basic-source-select.cpp" -#include "obs-app.hpp" + +#include "moc_OBSBasicSourceSelect.cpp" struct AddSourceData { /* Input data */ diff --git a/UI/window-basic-source-select.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp similarity index 94% rename from UI/window-basic-source-select.hpp rename to frontend/dialogs/OBSBasicSourceSelect.hpp index 3baadf7b3..077d8d298 100644 --- a/UI/window-basic-source-select.hpp +++ b/frontend/dialogs/OBSBasicSourceSelect.hpp @@ -17,14 +17,14 @@ #pragma once -#include -#include - #include "ui_OBSBasicSourceSelect.h" -#include "undo-stack-obs.hpp" -#include "window-basic-main.hpp" -class OBSBasic; +#include +#include + +#include + +#include class OBSBasicSourceSelect : public QDialog { Q_OBJECT diff --git a/UI/window-basic-transform.cpp b/frontend/dialogs/OBSBasicTransform.cpp similarity index 98% rename from UI/window-basic-transform.cpp rename to frontend/dialogs/OBSBasicTransform.cpp index 2e84f3519..173a68dc6 100644 --- a/UI/window-basic-transform.cpp +++ b/frontend/dialogs/OBSBasicTransform.cpp @@ -1,9 +1,8 @@ -#include -#include "window-basic-transform.hpp" -#include "window-basic-main.hpp" +#include "OBSBasicTransform.hpp" -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); +#include + +#include "moc_OBSBasicTransform.cpp" static bool find_sel(obs_scene_t *, obs_sceneitem_t *item, void *param) { diff --git a/UI/window-basic-transform.hpp b/frontend/dialogs/OBSBasicTransform.hpp similarity index 98% rename from UI/window-basic-transform.hpp rename to frontend/dialogs/OBSBasicTransform.hpp index 46905724f..d1559e18b 100644 --- a/UI/window-basic-transform.hpp +++ b/frontend/dialogs/OBSBasicTransform.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include - #include "ui_OBSBasicTransform.h" +#include + +#include + class OBSBasic; class QListWidgetItem; diff --git a/UI/window-basic-vcam-config.cpp b/frontend/dialogs/OBSBasicVCamConfig.cpp similarity index 95% rename from UI/window-basic-vcam-config.cpp rename to frontend/dialogs/OBSBasicVCamConfig.cpp index 7db63513f..3f7107499 100644 --- a/UI/window-basic-vcam-config.cpp +++ b/frontend/dialogs/OBSBasicVCamConfig.cpp @@ -1,11 +1,8 @@ -#include "moc_window-basic-vcam-config.cpp" -#include "window-basic-main.hpp" +#include "OBSBasicVCamConfig.hpp" -#include -#include -#include +#include -#include +#include "moc_OBSBasicVCamConfig.cpp" OBSBasicVCamConfig::OBSBasicVCamConfig(const VCamConfig &_config, bool _vcamActive, QWidget *parent) : config(_config), diff --git a/UI/window-basic-vcam-config.hpp b/frontend/dialogs/OBSBasicVCamConfig.hpp similarity index 85% rename from UI/window-basic-vcam-config.hpp rename to frontend/dialogs/OBSBasicVCamConfig.hpp index 7ca154c15..bd07700e7 100644 --- a/UI/window-basic-vcam-config.hpp +++ b/frontend/dialogs/OBSBasicVCamConfig.hpp @@ -1,14 +1,10 @@ #pragma once -#include -#include -#include - -#include "window-basic-vcam.hpp" - #include "ui_OBSBasicVCamConfig.h" -struct VCamConfig; +#include + +#include class OBSBasicVCamConfig : public QDialog { Q_OBJECT diff --git a/UI/window-log-reply.cpp b/frontend/dialogs/OBSLogReply.cpp similarity index 95% rename from UI/window-log-reply.cpp rename to frontend/dialogs/OBSLogReply.cpp index 71c379dfe..1000372a5 100644 --- a/UI/window-log-reply.cpp +++ b/frontend/dialogs/OBSLogReply.cpp @@ -15,12 +15,15 @@ along with this program. If not, see . ******************************************************************************/ +#include "OBSLogReply.hpp" + +#include + #include -#include -#include #include -#include "moc_window-log-reply.cpp" -#include "obs-app.hpp" +#include + +#include "moc_OBSLogReply.cpp" OBSLogReply::OBSLogReply(QWidget *parent, const QString &url, const bool crash) : QDialog(parent), diff --git a/UI/window-log-reply.hpp b/frontend/dialogs/OBSLogReply.hpp similarity index 98% rename from UI/window-log-reply.hpp rename to frontend/dialogs/OBSLogReply.hpp index 598a7f172..260b827f7 100644 --- a/UI/window-log-reply.hpp +++ b/frontend/dialogs/OBSLogReply.hpp @@ -17,9 +17,10 @@ #pragma once -#include #include "ui_OBSLogReply.h" +#include + class OBSLogReply : public QDialog { Q_OBJECT diff --git a/UI/log-viewer.cpp b/frontend/dialogs/OBSLogViewer.cpp similarity index 94% rename from UI/log-viewer.cpp rename to frontend/dialogs/OBSLogViewer.cpp index 9ef67a69c..592111c59 100644 --- a/UI/log-viewer.cpp +++ b/frontend/dialogs/OBSLogViewer.cpp @@ -1,16 +1,14 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSLogViewer.hpp" + +#include + #include -#include "moc_log-viewer.cpp" +#include +#include +#include + +#include "moc_OBSLogViewer.cpp" OBSLogViewer::OBSLogViewer(QWidget *parent) : QDialog(parent), ui(new Ui::OBSLogViewer) { diff --git a/UI/log-viewer.hpp b/frontend/dialogs/OBSLogViewer.hpp similarity index 88% rename from UI/log-viewer.hpp rename to frontend/dialogs/OBSLogViewer.hpp index d9e329bba..96a817100 100644 --- a/UI/log-viewer.hpp +++ b/frontend/dialogs/OBSLogViewer.hpp @@ -1,11 +1,9 @@ #pragma once -#include -#include -#include "obs-app.hpp" - #include "ui_OBSLogViewer.h" +#include + class OBSLogViewer : public QDialog { Q_OBJECT diff --git a/UI/window-permissions.cpp b/frontend/dialogs/OBSPermissions.cpp similarity index 97% rename from UI/window-permissions.cpp rename to frontend/dialogs/OBSPermissions.cpp index 046a011f9..9c62970d7 100644 --- a/UI/window-permissions.cpp +++ b/frontend/dialogs/OBSPermissions.cpp @@ -15,9 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include "moc_window-permissions.cpp" -#include "obs-app.hpp" +#include "OBSPermissions.hpp" + +#include + +#include "moc_OBSPermissions.cpp" OBSPermissions::OBSPermissions(QWidget *parent, MacPermissionStatus capture, MacPermissionStatus video, MacPermissionStatus audio, MacPermissionStatus accessibility) diff --git a/UI/window-permissions.hpp b/frontend/dialogs/OBSPermissions.hpp similarity index 96% rename from UI/window-permissions.hpp rename to frontend/dialogs/OBSPermissions.hpp index 67a74cbb1..5fab46b55 100644 --- a/UI/window-permissions.hpp +++ b/frontend/dialogs/OBSPermissions.hpp @@ -18,7 +18,10 @@ #pragma once #include "ui_OBSPermissions.h" -#include "platform.hpp" + +#include + +#include #define MACOS_PERMISSIONS_DIALOG_VERSION 1 diff --git a/UI/update/update-window.cpp b/frontend/dialogs/OBSUpdate.cpp similarity index 89% rename from UI/update/update-window.cpp rename to frontend/dialogs/OBSUpdate.cpp index 777eb7878..e25c425b6 100644 --- a/UI/update/update-window.cpp +++ b/frontend/dialogs/OBSUpdate.cpp @@ -1,5 +1,8 @@ -#include "update-window.hpp" -#include "obs-app.hpp" +#include "OBSUpdate.hpp" + +#include + +#include "ui_OBSUpdate.h" OBSUpdate::OBSUpdate(QWidget *parent, bool manualUpdate, const QString &text) : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint), diff --git a/UI/update/update-window.hpp b/frontend/dialogs/OBSUpdate.hpp similarity index 89% rename from UI/update/update-window.hpp rename to frontend/dialogs/OBSUpdate.hpp index 8c269762b..5f45674fd 100644 --- a/UI/update/update-window.hpp +++ b/frontend/dialogs/OBSUpdate.hpp @@ -1,9 +1,8 @@ #pragma once #include -#include -#include "ui_OBSUpdate.h" +class Ui_OBSUpdate; class OBSUpdate : public QDialog { Q_OBJECT diff --git a/UI/window-whats-new.cpp b/frontend/dialogs/OBSWhatsNew.cpp similarity index 90% rename from UI/window-whats-new.cpp rename to frontend/dialogs/OBSWhatsNew.cpp index a39fc0b51..22ba4e29b 100644 --- a/UI/window-whats-new.cpp +++ b/frontend/dialogs/OBSWhatsNew.cpp @@ -1,17 +1,17 @@ -#include "moc_window-whats-new.cpp" +#include "OBSWhatsNew.hpp" -#include -#include -#include - -#include "window-basic-main.hpp" +#include #ifdef BROWSER_AVAILABLE #include extern QCef *cef; #endif -/* ------------------------------------------------------------------------- */ +#include +#include +#include + +#include "moc_OBSWhatsNew.cpp" OBSWhatsNew::OBSWhatsNew(QWidget *parent, const std::string &url) : QDialog(parent) { diff --git a/UI/window-whats-new.hpp b/frontend/dialogs/OBSWhatsNew.hpp similarity index 99% rename from UI/window-whats-new.hpp rename to frontend/dialogs/OBSWhatsNew.hpp index 1c39a850c..6ff1081aa 100644 --- a/UI/window-whats-new.hpp +++ b/frontend/dialogs/OBSWhatsNew.hpp @@ -2,6 +2,7 @@ #include #include + #include class QCefWidget; diff --git a/UI/window-youtube-actions.cpp b/frontend/dialogs/OBSYoutubeActions.cpp similarity index 99% rename from UI/window-youtube-actions.cpp rename to frontend/dialogs/OBSYoutubeActions.cpp index 9c8533fd0..27ceef450 100644 --- a/UI/window-youtube-actions.cpp +++ b/frontend/dialogs/OBSYoutubeActions.cpp @@ -1,16 +1,17 @@ -#include "window-basic-main.hpp" -#include "moc_window-youtube-actions.cpp" +#include "OBSYoutubeActions.hpp" -#include "obs-app.hpp" -#include "youtube-api-wrappers.hpp" +#include +#include #include -#include -#include + #include #include -#include #include +#include +#include + +#include "moc_OBSYoutubeActions.cpp" const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'"; const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m"; diff --git a/UI/window-youtube-actions.hpp b/frontend/dialogs/OBSYoutubeActions.hpp similarity index 96% rename from UI/window-youtube-actions.hpp rename to frontend/dialogs/OBSYoutubeActions.hpp index 7543792a8..323c25d52 100644 --- a/UI/window-youtube-actions.hpp +++ b/frontend/dialogs/OBSYoutubeActions.hpp @@ -1,11 +1,10 @@ #pragma once -#include -#include -#include - #include "ui_OBSYoutubeActions.h" -#include "youtube-api-wrappers.hpp" + +#include + +#include class WorkerThread : public QThread { Q_OBJECT From 1ff68267a3efb216f857743e923490d870c80ff4 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 21:03:53 +0100 Subject: [PATCH 16/37] frontend: Split Settings implementation into single files per C++ class --- frontend/components/SilentUpdateCheckBox.hpp | 458 +- frontend/components/SilentUpdateSpinBox.hpp | 458 +- frontend/settings/OBSBasicSettings.cpp | 118 +- frontend/settings/OBSBasicSettings.hpp | 55 +- frontend/settings/OBSHotkeyEdit.cpp | 269 +- frontend/settings/OBSHotkeyEdit.hpp | 98 +- frontend/settings/OBSHotkeyLabel.cpp | 409 +- frontend/settings/OBSHotkeyLabel.hpp | 157 +- frontend/settings/OBSHotkeyWidget.cpp | 276 +- frontend/settings/OBSHotkeyWidget.hpp | 100 +- frontend/utility/SettingsEventFilter.hpp | 5784 +----------------- 11 files changed, 104 insertions(+), 8078 deletions(-) diff --git a/frontend/components/SilentUpdateCheckBox.hpp b/frontend/components/SilentUpdateCheckBox.hpp index 05d46fd41..ebbf1c916 100644 --- a/frontend/components/SilentUpdateCheckBox.hpp +++ b/frontend/components/SilentUpdateCheckBox.hpp @@ -18,33 +18,7 @@ #pragma once -#include -#include -#include -#include -#include - -#include - -#include "auth-base.hpp" -#include "ffmpeg-utils.hpp" -#include "obs-app-theming.hpp" - -class OBSBasic; -class QAbstractButton; -class QRadioButton; -class QComboBox; -class QCheckBox; -class QLabel; -class QButtonGroup; -class OBSPropertiesView; -class OBSHotkeyWidget; - -#include "ui_OBSBasicSettings.h" - -#define VOLUME_METER_DECAY_FAST 23.53 -#define VOLUME_METER_DECAY_MEDIUM 11.76 -#define VOLUME_METER_DECAY_SLOW 8.57 +#include class SilentUpdateCheckBox : public QCheckBox { Q_OBJECT @@ -57,433 +31,3 @@ public slots: blockSignals(blocked); } }; - -class SilentUpdateSpinBox : public QSpinBox { - Q_OBJECT - -public slots: - void setValueSilently(int val) - { - bool blocked = blockSignals(true); - setValue(val); - blockSignals(blocked); - } -}; - -std::string DeserializeConfigText(const char *value); - -class OBSBasicSettings : public QDialog { - Q_OBJECT - Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true) - Q_PROPERTY(QIcon appearanceIcon READ GetAppearanceIcon WRITE SetAppearanceIcon DESIGNABLE true) - Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true) - Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true) - Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true) - Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true) - Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true) - Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true) - - enum Pages { GENERAL, APPEARANCE, STREAM, OUTPUT, AUDIO, VIDEO, HOTKEYS, ACCESSIBILITY, ADVANCED, NUM_PAGES }; - -private: - OBSBasic *main; - - std::unique_ptr ui; - - std::shared_ptr auth; - - bool generalChanged = false; - bool stream1Changed = false; - bool outputsChanged = false; - bool audioChanged = false; - bool videoChanged = false; - bool hotkeysChanged = false; - bool a11yChanged = false; - bool appearanceChanged = false; - bool advancedChanged = false; - int pageIndex = 0; - bool loading = true; - bool forceAuthReload = false; - bool forceUpdateCheck = false; - int sampleRateIndex = 0; - int channelIndex = 0; - bool llBufferingEnabled = false; - bool hotkeysLoaded = false; - - int lastSimpleRecQualityIdx = 0; - int lastServiceIdx = -1; - int lastIgnoreRecommended = -1; - int lastChannelSetupIdx = 0; - - static constexpr uint32_t ENCODER_HIDE_FLAGS = (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL); - - OBSTheme *savedTheme = nullptr; - - std::vector formats; - - OBSPropertiesView *streamProperties = nullptr; - OBSPropertiesView *streamEncoderProps = nullptr; - OBSPropertiesView *recordEncoderProps = nullptr; - - QPointer advOutRecWarning; - QPointer simpleOutRecWarning; - - QString curPreset; - QString curQSVPreset; - QString curNVENCPreset; - QString curAMDPreset; - QString curAMDAV1Preset; - - QString curAdvStreamEncoder; - QString curAdvRecordEncoder; - - using AudioSource_t = std::tuple, QPointer, QPointer, - QPointer>; - std::vector audioSources; - std::vector audioSourceSignals; - OBSSignal sourceCreated; - OBSSignal channelChanged; - - std::vector>> hotkeys; - OBSSignal hotkeyRegistered; - OBSSignal hotkeyUnregistered; - - uint32_t outputCX = 0; - uint32_t outputCY = 0; - - QPointer simpleVodTrack; - - QPointer vodTrackCheckbox; - QPointer vodTrackContainer; - QPointer vodTrack[MAX_AUDIO_MIXES]; - - QIcon hotkeyConflictIcon; - - void SaveCombo(QComboBox *widget, const char *section, const char *value); - void SaveComboData(QComboBox *widget, const char *section, const char *value); - void SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert = false); - void SaveGroupBox(QGroupBox *widget, const char *section, const char *value); - void SaveEdit(QLineEdit *widget, const char *section, const char *value); - void SaveSpinBox(QSpinBox *widget, const char *section, const char *value); - void SaveText(QPlainTextEdit *widget, const char *section, const char *value); - void SaveFormat(QComboBox *combo); - void SaveEncoder(QComboBox *combo, const char *section, const char *value); - - bool ResFPSValid(obs_service_resolution *res_list, size_t res_count, int max_fps); - void ClosestResFPS(obs_service_resolution *res_list, size_t res_count, int max_fps, int &new_cx, int &new_cy, - int &new_fps); - - inline bool Changed() const - { - return generalChanged || appearanceChanged || outputsChanged || stream1Changed || audioChanged || - videoChanged || advancedChanged || hotkeysChanged || a11yChanged; - } - - inline void EnableApplyButton(bool en) { ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(en); } - - inline void ClearChanged() - { - generalChanged = false; - stream1Changed = false; - outputsChanged = false; - audioChanged = false; - videoChanged = false; - hotkeysChanged = false; - a11yChanged = false; - advancedChanged = false; - appearanceChanged = false; - EnableApplyButton(false); - } - - template - void HookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), - void (OBSBasicSettings::*slot)(SlotArgs...)) - { - QObject::connect(widget, signal, this, slot); - widget->setProperty("changed", QVariant(false)); - } - - bool QueryChanges(); - bool QueryAllowedToClose(); - - void ResetEncoders(bool streamOnly = false); - void LoadColorRanges(); - void LoadColorSpaces(); - void LoadColorFormats(); - void LoadFormats(); - void ReloadCodecs(const FFmpegFormat &format); - - void UpdateColorFormatSpaceWarning(); - - void LoadGeneralSettings(); - void LoadStream1Settings(); - void LoadOutputSettings(); - void LoadAudioSettings(); - void LoadVideoSettings(); - void LoadHotkeySettings(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); - void LoadA11ySettings(bool presetChange = false); - void LoadAppearanceSettings(bool reload = false); - void LoadAdvancedSettings(); - void LoadSettings(bool changedOnly); - - OBSPropertiesView *CreateEncoderPropertyView(const char *encoder, const char *path, bool changed = false); - - /* general */ - void LoadLanguageList(); - void LoadThemeList(bool firstLoad); - void LoadBranchesList(); - - /* stream */ - void InitStreamPage(); - bool IsCustomService() const; - inline bool IsWHIP() const; - void LoadServices(bool showAll); - void OnOAuthStreamKeyConnected(); - void OnAuthConnected(); - QString lastService; - QString protocol; - QString lastCustomServer; - int prevLangIndex; - bool prevBrowserAccel; - - void ServiceChanged(bool resetFields = false); - QString FindProtocol(); - void UpdateServerList(); - void UpdateKeyLink(); - void UpdateVodTrackSetting(); - void UpdateServiceRecommendations(); - void UpdateMoreInfoLink(); - void UpdateAdvNetworkGroup(); - - /* Appearance */ - void InitAppearancePage(); - - bool IsCustomServer(); - -private slots: - void UpdateMultitrackVideo(); - void RecreateOutputResolutionWidget(); - bool UpdateResFPSLimits(); - void DisplayEnforceWarning(bool checked); - void on_show_clicked(); - void on_authPwShow_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_useAuth_toggled(); - void on_server_currentIndexChanged(int index); - - void on_hotkeyFilterReset_clicked(); - void on_hotkeyFilterSearch_textChanged(const QString text); - void on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo); - -private: - /* output */ - void LoadSimpleOutputSettings(); - void LoadAdvOutputStreamingSettings(); - void LoadAdvOutputStreamingEncoderProperties(); - void LoadAdvOutputRecordingSettings(); - void LoadAdvOutputRecordingEncoderProperties(); - void LoadAdvOutputFFmpegSettings(); - void LoadAdvOutputAudioSettings(); - void SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncode = false); - - /* audio */ - void LoadListValues(QComboBox *widget, obs_property_t *prop, int index); - void LoadAudioDevices(); - void LoadAudioSources(); - - /* video */ - void LoadRendererList(); - void ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals = false); - void LoadDownscaleFilters(); - void LoadResolutionLists(); - void LoadFPSData(); - - /* a11y */ - void UpdateA11yColors(); - void SetDefaultColors(); - void ResetDefaultColors(); - QColor GetColor(uint32_t colorVal, QString label); - uint32_t preset = 0; - uint32_t selectRed = 0x0000FF; - uint32_t selectGreen = 0x00FF00; - uint32_t selectBlue = 0xFF7F00; - uint32_t mixerGreen = 0x267f26; - uint32_t mixerYellow = 0x267f7f; - uint32_t mixerRed = 0x26267f; - uint32_t mixerGreenActive = 0x4cff4c; - uint32_t mixerYellowActive = 0x4cffff; - uint32_t mixerRedActive = 0x4c4cff; - - void SaveGeneralSettings(); - void SaveStream1Settings(); - void SaveOutputSettings(); - void SaveAudioSettings(); - void SaveVideoSettings(); - void SaveHotkeySettings(); - void SaveA11ySettings(); - void SaveAppearanceSettings(); - void SaveAdvancedSettings(); - void SaveSettings(); - - void SearchHotkeys(const QString &text, obs_key_combination_t filterCombo); - - void UpdateSimpleOutStreamDelayEstimate(); - void UpdateAdvOutStreamDelayEstimate(); - - void FillSimpleRecordingValues(); - void FillAudioMonitoringDevices(); - - void RecalcOutputResPixels(const char *resText); - - bool AskIfCanCloseSettings(); - - void UpdateYouTubeAppDockSettings(); - - QIcon generalIcon; - QIcon appearanceIcon; - QIcon streamIcon; - QIcon outputIcon; - QIcon audioIcon; - QIcon videoIcon; - QIcon hotkeysIcon; - QIcon accessibilityIcon; - QIcon advancedIcon; - - QIcon GetGeneralIcon() const; - QIcon GetAppearanceIcon() const; - QIcon GetStreamIcon() const; - QIcon GetOutputIcon() const; - QIcon GetAudioIcon() const; - QIcon GetVideoIcon() const; - QIcon GetHotkeysIcon() const; - QIcon GetAccessibilityIcon() const; - QIcon GetAdvancedIcon() const; - - int CurrentFLVTrack(); - int SimpleOutGetSelectedAudioTracks(); - int AdvOutGetSelectedAudioTracks(); - int AdvOutGetStreamingSelectedAudioTracks(); - - OBSService GetStream1Service(); - - bool ServiceAndVCodecCompatible(); - bool ServiceAndACodecCompatible(); - bool ServiceSupportsCodecCheck(); - - inline bool AllowsMultiTrack(const char *protocol); - void SwapMultiTrack(const char *protocol); - -private slots: - void on_theme_activated(int idx); - void on_themeVariant_activated(int idx); - - void on_listWidget_itemSelectionChanged(); - void on_buttonBox_clicked(QAbstractButton *button); - - void on_service_currentIndexChanged(int idx); - void on_customServer_textChanged(const QString &text); - void on_simpleOutputBrowse_clicked(); - void on_advOutRecPathBrowse_clicked(); - void on_advOutFFPathBrowse_clicked(); - void on_advOutEncoder_currentIndexChanged(); - void on_advOutRecEncoder_currentIndexChanged(int idx); - void on_advOutFFIgnoreCompat_stateChanged(int state); - void on_advOutFFFormat_currentIndexChanged(int idx); - void on_advOutFFAEncoder_currentIndexChanged(int idx); - void on_advOutFFVEncoder_currentIndexChanged(int idx); - void on_advOutFFType_currentIndexChanged(int idx); - - void on_colorFormat_currentIndexChanged(int idx); - void on_colorSpace_currentIndexChanged(int idx); - - void on_filenameFormatting_textEdited(const QString &text); - void on_outputResolution_editTextChanged(const QString &text); - void on_baseResolution_editTextChanged(const QString &text); - - void on_disableOSXVSync_clicked(); - - void on_choose1_clicked(); - void on_choose2_clicked(); - void on_choose3_clicked(); - void on_choose4_clicked(); - void on_choose5_clicked(); - void on_choose6_clicked(); - void on_choose7_clicked(); - void on_choose8_clicked(); - void on_choose9_clicked(); - void on_colorPreset_currentIndexChanged(int idx); - - void GeneralChanged(); - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - void HideOBSWindowWarning(Qt::CheckState state); -#else - void HideOBSWindowWarning(int state); -#endif - void AudioChanged(); - void AudioChangedRestart(); - void ReloadAudioSources(); - void SurroundWarning(int idx); - void SpeakerLayoutChanged(int idx); - void LowLatencyBufferingChanged(bool checked); - void UpdateAudioWarnings(); - void OutputsChanged(); - void Stream1Changed(); - void VideoChanged(); - void VideoChangedResolution(); - void HotkeysChanged(); - bool ScanDuplicateHotkeys(QFormLayout *layout); - void ReloadHotkeys(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); - void A11yChanged(); - void AppearanceChanged(); - void AdvancedChanged(); - void AdvancedChangedRestart(); - - void UpdateStreamDelayEstimate(); - - void UpdateAutomaticReplayBufferCheckboxes(); - - void AdvOutSplitFileChanged(); - void AdvOutRecCheckWarnings(); - void AdvOutRecCheckCodecs(); - - void SimpleRecordingQualityChanged(); - void SimpleRecordingEncoderChanged(); - void SimpleRecordingQualityLosslessWarning(int idx); - - void SimpleReplayBufferChanged(); - void AdvReplayBufferChanged(); - - void SimpleStreamingEncoderChanged(); - - OBSService SpawnTempService(); - - void SetGeneralIcon(const QIcon &icon); - void SetAppearanceIcon(const QIcon &icon); - void SetStreamIcon(const QIcon &icon); - void SetOutputIcon(const QIcon &icon); - void SetAudioIcon(const QIcon &icon); - void SetVideoIcon(const QIcon &icon); - void SetHotkeysIcon(const QIcon &icon); - void SetAccessibilityIcon(const QIcon &icon); - void SetAdvancedIcon(const QIcon &icon); - - void UseStreamKeyAdvClicked(); - - void SimpleStreamAudioEncoderChanged(); - void AdvAudioEncodersChanged(); - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual void showEvent(QShowEvent *event) override; - void reject() override; - -public: - OBSBasicSettings(QWidget *parent); - ~OBSBasicSettings(); - - inline const QIcon &GetHotkeyConflictIcon() const { return hotkeyConflictIcon; } -}; diff --git a/frontend/components/SilentUpdateSpinBox.hpp b/frontend/components/SilentUpdateSpinBox.hpp index 05d46fd41..eadbc9cc0 100644 --- a/frontend/components/SilentUpdateSpinBox.hpp +++ b/frontend/components/SilentUpdateSpinBox.hpp @@ -18,45 +18,7 @@ #pragma once -#include -#include -#include -#include -#include - -#include - -#include "auth-base.hpp" -#include "ffmpeg-utils.hpp" -#include "obs-app-theming.hpp" - -class OBSBasic; -class QAbstractButton; -class QRadioButton; -class QComboBox; -class QCheckBox; -class QLabel; -class QButtonGroup; -class OBSPropertiesView; -class OBSHotkeyWidget; - -#include "ui_OBSBasicSettings.h" - -#define VOLUME_METER_DECAY_FAST 23.53 -#define VOLUME_METER_DECAY_MEDIUM 11.76 -#define VOLUME_METER_DECAY_SLOW 8.57 - -class SilentUpdateCheckBox : public QCheckBox { - Q_OBJECT - -public slots: - void setCheckedSilently(bool checked) - { - bool blocked = blockSignals(true); - setChecked(checked); - blockSignals(blocked); - } -}; +#include class SilentUpdateSpinBox : public QSpinBox { Q_OBJECT @@ -69,421 +31,3 @@ public slots: blockSignals(blocked); } }; - -std::string DeserializeConfigText(const char *value); - -class OBSBasicSettings : public QDialog { - Q_OBJECT - Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true) - Q_PROPERTY(QIcon appearanceIcon READ GetAppearanceIcon WRITE SetAppearanceIcon DESIGNABLE true) - Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true) - Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true) - Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true) - Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true) - Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true) - Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true) - - enum Pages { GENERAL, APPEARANCE, STREAM, OUTPUT, AUDIO, VIDEO, HOTKEYS, ACCESSIBILITY, ADVANCED, NUM_PAGES }; - -private: - OBSBasic *main; - - std::unique_ptr ui; - - std::shared_ptr auth; - - bool generalChanged = false; - bool stream1Changed = false; - bool outputsChanged = false; - bool audioChanged = false; - bool videoChanged = false; - bool hotkeysChanged = false; - bool a11yChanged = false; - bool appearanceChanged = false; - bool advancedChanged = false; - int pageIndex = 0; - bool loading = true; - bool forceAuthReload = false; - bool forceUpdateCheck = false; - int sampleRateIndex = 0; - int channelIndex = 0; - bool llBufferingEnabled = false; - bool hotkeysLoaded = false; - - int lastSimpleRecQualityIdx = 0; - int lastServiceIdx = -1; - int lastIgnoreRecommended = -1; - int lastChannelSetupIdx = 0; - - static constexpr uint32_t ENCODER_HIDE_FLAGS = (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL); - - OBSTheme *savedTheme = nullptr; - - std::vector formats; - - OBSPropertiesView *streamProperties = nullptr; - OBSPropertiesView *streamEncoderProps = nullptr; - OBSPropertiesView *recordEncoderProps = nullptr; - - QPointer advOutRecWarning; - QPointer simpleOutRecWarning; - - QString curPreset; - QString curQSVPreset; - QString curNVENCPreset; - QString curAMDPreset; - QString curAMDAV1Preset; - - QString curAdvStreamEncoder; - QString curAdvRecordEncoder; - - using AudioSource_t = std::tuple, QPointer, QPointer, - QPointer>; - std::vector audioSources; - std::vector audioSourceSignals; - OBSSignal sourceCreated; - OBSSignal channelChanged; - - std::vector>> hotkeys; - OBSSignal hotkeyRegistered; - OBSSignal hotkeyUnregistered; - - uint32_t outputCX = 0; - uint32_t outputCY = 0; - - QPointer simpleVodTrack; - - QPointer vodTrackCheckbox; - QPointer vodTrackContainer; - QPointer vodTrack[MAX_AUDIO_MIXES]; - - QIcon hotkeyConflictIcon; - - void SaveCombo(QComboBox *widget, const char *section, const char *value); - void SaveComboData(QComboBox *widget, const char *section, const char *value); - void SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert = false); - void SaveGroupBox(QGroupBox *widget, const char *section, const char *value); - void SaveEdit(QLineEdit *widget, const char *section, const char *value); - void SaveSpinBox(QSpinBox *widget, const char *section, const char *value); - void SaveText(QPlainTextEdit *widget, const char *section, const char *value); - void SaveFormat(QComboBox *combo); - void SaveEncoder(QComboBox *combo, const char *section, const char *value); - - bool ResFPSValid(obs_service_resolution *res_list, size_t res_count, int max_fps); - void ClosestResFPS(obs_service_resolution *res_list, size_t res_count, int max_fps, int &new_cx, int &new_cy, - int &new_fps); - - inline bool Changed() const - { - return generalChanged || appearanceChanged || outputsChanged || stream1Changed || audioChanged || - videoChanged || advancedChanged || hotkeysChanged || a11yChanged; - } - - inline void EnableApplyButton(bool en) { ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(en); } - - inline void ClearChanged() - { - generalChanged = false; - stream1Changed = false; - outputsChanged = false; - audioChanged = false; - videoChanged = false; - hotkeysChanged = false; - a11yChanged = false; - advancedChanged = false; - appearanceChanged = false; - EnableApplyButton(false); - } - - template - void HookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), - void (OBSBasicSettings::*slot)(SlotArgs...)) - { - QObject::connect(widget, signal, this, slot); - widget->setProperty("changed", QVariant(false)); - } - - bool QueryChanges(); - bool QueryAllowedToClose(); - - void ResetEncoders(bool streamOnly = false); - void LoadColorRanges(); - void LoadColorSpaces(); - void LoadColorFormats(); - void LoadFormats(); - void ReloadCodecs(const FFmpegFormat &format); - - void UpdateColorFormatSpaceWarning(); - - void LoadGeneralSettings(); - void LoadStream1Settings(); - void LoadOutputSettings(); - void LoadAudioSettings(); - void LoadVideoSettings(); - void LoadHotkeySettings(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); - void LoadA11ySettings(bool presetChange = false); - void LoadAppearanceSettings(bool reload = false); - void LoadAdvancedSettings(); - void LoadSettings(bool changedOnly); - - OBSPropertiesView *CreateEncoderPropertyView(const char *encoder, const char *path, bool changed = false); - - /* general */ - void LoadLanguageList(); - void LoadThemeList(bool firstLoad); - void LoadBranchesList(); - - /* stream */ - void InitStreamPage(); - bool IsCustomService() const; - inline bool IsWHIP() const; - void LoadServices(bool showAll); - void OnOAuthStreamKeyConnected(); - void OnAuthConnected(); - QString lastService; - QString protocol; - QString lastCustomServer; - int prevLangIndex; - bool prevBrowserAccel; - - void ServiceChanged(bool resetFields = false); - QString FindProtocol(); - void UpdateServerList(); - void UpdateKeyLink(); - void UpdateVodTrackSetting(); - void UpdateServiceRecommendations(); - void UpdateMoreInfoLink(); - void UpdateAdvNetworkGroup(); - - /* Appearance */ - void InitAppearancePage(); - - bool IsCustomServer(); - -private slots: - void UpdateMultitrackVideo(); - void RecreateOutputResolutionWidget(); - bool UpdateResFPSLimits(); - void DisplayEnforceWarning(bool checked); - void on_show_clicked(); - void on_authPwShow_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_useAuth_toggled(); - void on_server_currentIndexChanged(int index); - - void on_hotkeyFilterReset_clicked(); - void on_hotkeyFilterSearch_textChanged(const QString text); - void on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo); - -private: - /* output */ - void LoadSimpleOutputSettings(); - void LoadAdvOutputStreamingSettings(); - void LoadAdvOutputStreamingEncoderProperties(); - void LoadAdvOutputRecordingSettings(); - void LoadAdvOutputRecordingEncoderProperties(); - void LoadAdvOutputFFmpegSettings(); - void LoadAdvOutputAudioSettings(); - void SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncode = false); - - /* audio */ - void LoadListValues(QComboBox *widget, obs_property_t *prop, int index); - void LoadAudioDevices(); - void LoadAudioSources(); - - /* video */ - void LoadRendererList(); - void ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals = false); - void LoadDownscaleFilters(); - void LoadResolutionLists(); - void LoadFPSData(); - - /* a11y */ - void UpdateA11yColors(); - void SetDefaultColors(); - void ResetDefaultColors(); - QColor GetColor(uint32_t colorVal, QString label); - uint32_t preset = 0; - uint32_t selectRed = 0x0000FF; - uint32_t selectGreen = 0x00FF00; - uint32_t selectBlue = 0xFF7F00; - uint32_t mixerGreen = 0x267f26; - uint32_t mixerYellow = 0x267f7f; - uint32_t mixerRed = 0x26267f; - uint32_t mixerGreenActive = 0x4cff4c; - uint32_t mixerYellowActive = 0x4cffff; - uint32_t mixerRedActive = 0x4c4cff; - - void SaveGeneralSettings(); - void SaveStream1Settings(); - void SaveOutputSettings(); - void SaveAudioSettings(); - void SaveVideoSettings(); - void SaveHotkeySettings(); - void SaveA11ySettings(); - void SaveAppearanceSettings(); - void SaveAdvancedSettings(); - void SaveSettings(); - - void SearchHotkeys(const QString &text, obs_key_combination_t filterCombo); - - void UpdateSimpleOutStreamDelayEstimate(); - void UpdateAdvOutStreamDelayEstimate(); - - void FillSimpleRecordingValues(); - void FillAudioMonitoringDevices(); - - void RecalcOutputResPixels(const char *resText); - - bool AskIfCanCloseSettings(); - - void UpdateYouTubeAppDockSettings(); - - QIcon generalIcon; - QIcon appearanceIcon; - QIcon streamIcon; - QIcon outputIcon; - QIcon audioIcon; - QIcon videoIcon; - QIcon hotkeysIcon; - QIcon accessibilityIcon; - QIcon advancedIcon; - - QIcon GetGeneralIcon() const; - QIcon GetAppearanceIcon() const; - QIcon GetStreamIcon() const; - QIcon GetOutputIcon() const; - QIcon GetAudioIcon() const; - QIcon GetVideoIcon() const; - QIcon GetHotkeysIcon() const; - QIcon GetAccessibilityIcon() const; - QIcon GetAdvancedIcon() const; - - int CurrentFLVTrack(); - int SimpleOutGetSelectedAudioTracks(); - int AdvOutGetSelectedAudioTracks(); - int AdvOutGetStreamingSelectedAudioTracks(); - - OBSService GetStream1Service(); - - bool ServiceAndVCodecCompatible(); - bool ServiceAndACodecCompatible(); - bool ServiceSupportsCodecCheck(); - - inline bool AllowsMultiTrack(const char *protocol); - void SwapMultiTrack(const char *protocol); - -private slots: - void on_theme_activated(int idx); - void on_themeVariant_activated(int idx); - - void on_listWidget_itemSelectionChanged(); - void on_buttonBox_clicked(QAbstractButton *button); - - void on_service_currentIndexChanged(int idx); - void on_customServer_textChanged(const QString &text); - void on_simpleOutputBrowse_clicked(); - void on_advOutRecPathBrowse_clicked(); - void on_advOutFFPathBrowse_clicked(); - void on_advOutEncoder_currentIndexChanged(); - void on_advOutRecEncoder_currentIndexChanged(int idx); - void on_advOutFFIgnoreCompat_stateChanged(int state); - void on_advOutFFFormat_currentIndexChanged(int idx); - void on_advOutFFAEncoder_currentIndexChanged(int idx); - void on_advOutFFVEncoder_currentIndexChanged(int idx); - void on_advOutFFType_currentIndexChanged(int idx); - - void on_colorFormat_currentIndexChanged(int idx); - void on_colorSpace_currentIndexChanged(int idx); - - void on_filenameFormatting_textEdited(const QString &text); - void on_outputResolution_editTextChanged(const QString &text); - void on_baseResolution_editTextChanged(const QString &text); - - void on_disableOSXVSync_clicked(); - - void on_choose1_clicked(); - void on_choose2_clicked(); - void on_choose3_clicked(); - void on_choose4_clicked(); - void on_choose5_clicked(); - void on_choose6_clicked(); - void on_choose7_clicked(); - void on_choose8_clicked(); - void on_choose9_clicked(); - void on_colorPreset_currentIndexChanged(int idx); - - void GeneralChanged(); - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - void HideOBSWindowWarning(Qt::CheckState state); -#else - void HideOBSWindowWarning(int state); -#endif - void AudioChanged(); - void AudioChangedRestart(); - void ReloadAudioSources(); - void SurroundWarning(int idx); - void SpeakerLayoutChanged(int idx); - void LowLatencyBufferingChanged(bool checked); - void UpdateAudioWarnings(); - void OutputsChanged(); - void Stream1Changed(); - void VideoChanged(); - void VideoChangedResolution(); - void HotkeysChanged(); - bool ScanDuplicateHotkeys(QFormLayout *layout); - void ReloadHotkeys(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID); - void A11yChanged(); - void AppearanceChanged(); - void AdvancedChanged(); - void AdvancedChangedRestart(); - - void UpdateStreamDelayEstimate(); - - void UpdateAutomaticReplayBufferCheckboxes(); - - void AdvOutSplitFileChanged(); - void AdvOutRecCheckWarnings(); - void AdvOutRecCheckCodecs(); - - void SimpleRecordingQualityChanged(); - void SimpleRecordingEncoderChanged(); - void SimpleRecordingQualityLosslessWarning(int idx); - - void SimpleReplayBufferChanged(); - void AdvReplayBufferChanged(); - - void SimpleStreamingEncoderChanged(); - - OBSService SpawnTempService(); - - void SetGeneralIcon(const QIcon &icon); - void SetAppearanceIcon(const QIcon &icon); - void SetStreamIcon(const QIcon &icon); - void SetOutputIcon(const QIcon &icon); - void SetAudioIcon(const QIcon &icon); - void SetVideoIcon(const QIcon &icon); - void SetHotkeysIcon(const QIcon &icon); - void SetAccessibilityIcon(const QIcon &icon); - void SetAdvancedIcon(const QIcon &icon); - - void UseStreamKeyAdvClicked(); - - void SimpleStreamAudioEncoderChanged(); - void AdvAudioEncodersChanged(); - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual void showEvent(QShowEvent *event) override; - void reject() override; - -public: - OBSBasicSettings(QWidget *parent); - ~OBSBasicSettings(); - - inline const QIcon &GetHotkeyConflictIcon() const { return hotkeyConflictIcon; } -}; diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp index d8a35501b..dc7bc73fd 100644 --- a/frontend/settings/OBSBasicSettings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -1,88 +1,60 @@ /****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Philippe Groarke + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ - 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. +#include "OBSBasicSettings.hpp" +#include "OBSHotkeyLabel.hpp" +#include "OBSHotkeyWidget.hpp" - 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. +#include +#include +#include +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include +#include +#include +#include +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include +#include - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include "audio-encoders.hpp" -#include "hotkey-edit.hpp" -#include "source-label.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "properties-view.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-settings.cpp" -#include "window-basic-main-outputs.hpp" -#include "window-projector.hpp" +#include +#include -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif +#include -#include -#include -#include "ui-config.h" +#include "moc_OBSBasicSettings.cpp" using namespace std; -class SettingsEventFilter : public QObject { - QScopedPointer shortcutFilter; +extern const char *get_simple_output_encoder(const char *encoder); -public: - inline SettingsEventFilter() : shortcutFilter((OBSEventFilter *)CreateShortcutFilter()) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) override - { - int key; - - switch (event->type()) { - case QEvent::KeyPress: - case QEvent::KeyRelease: - key = static_cast(event)->key(); - if (key == Qt::Key_Escape) { - return false; - } - default: - break; - } - - return shortcutFilter->filter(obj, event); - } -}; +extern bool restart; +extern bool opt_allow_opengl; +extern bool cef_js_avail; static inline bool ResTooHigh(uint32_t cx, uint32_t cy) { diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp index 47e8df795..08664e0d7 100644 --- a/frontend/settings/OBSBasicSettings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -18,57 +18,22 @@ #pragma once -#include -#include -#include -#include -#include - -#include - -#include "auth-base.hpp" -#include "ffmpeg-utils.hpp" -#include "obs-app-theming.hpp" - -class OBSBasic; -class QAbstractButton; -class QRadioButton; -class QComboBox; -class QCheckBox; -class QLabel; -class QButtonGroup; -class OBSPropertiesView; -class OBSHotkeyWidget; - #include "ui_OBSBasicSettings.h" +#include + +#include + #define VOLUME_METER_DECAY_FAST 23.53 #define VOLUME_METER_DECAY_MEDIUM 11.76 #define VOLUME_METER_DECAY_SLOW 8.57 -class SilentUpdateCheckBox : public QCheckBox { - Q_OBJECT - -public slots: - void setCheckedSilently(bool checked) - { - bool blocked = blockSignals(true); - setChecked(checked); - blockSignals(blocked); - } -}; - -class SilentUpdateSpinBox : public QSpinBox { - Q_OBJECT - -public slots: - void setValueSilently(int val) - { - bool blocked = blockSignals(true); - setValue(val); - blockSignals(blocked); - } -}; +class Auth; +class OBSBasic; +class OBSHotkeyWidget; +class OBSPropertiesView; +struct FFmpegFormat; +struct OBSTheme; std::string DeserializeConfigText(const char *value); diff --git a/frontend/settings/OBSHotkeyEdit.cpp b/frontend/settings/OBSHotkeyEdit.cpp index 5dc5355e7..336f416c8 100644 --- a/frontend/settings/OBSHotkeyEdit.cpp +++ b/frontend/settings/OBSHotkeyEdit.cpp @@ -15,16 +15,17 @@ along with this program. If not, see . ******************************************************************************/ -#include "window-basic-settings.hpp" -#include "moc_hotkey-edit.cpp" +#include "OBSHotkeyEdit.hpp" +#include "OBSBasicSettings.hpp" + +#include -#include -#include -#include -#include #include +#include -#include "obs-app.hpp" +#include + +#include "moc_OBSHotkeyEdit.cpp" void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) { @@ -214,257 +215,3 @@ void OBSHotkeyEdit::ReloadKeyLayout() { RenderKey(); } - -void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) -{ - if (combos.empty()) - AddEdit({0, OBS_KEY_NONE}); - - for (auto combo : combos) - AddEdit(combo); -} - -bool OBSHotkeyWidget::Changed() const -{ - return changed || std::any_of(begin(edits), end(edits), [](OBSHotkeyEdit *edit) { return edit->changed; }); -} - -void OBSHotkeyWidget::Apply() -{ - for (auto &edit : edits) { - edit->original = edit->key; - edit->changed = false; - } - - changed = false; - - for (auto &revertButton : revertButtons) - revertButton->setEnabled(false); -} - -void OBSHotkeyWidget::GetCombinations(std::vector &combinations) const -{ - combinations.clear(); - for (auto &edit : edits) - if (!obs_key_combination_is_empty(edit->key)) - combinations.emplace_back(edit->key); -} - -void OBSHotkeyWidget::Save() -{ - std::vector combinations; - Save(combinations); -} - -void OBSHotkeyWidget::Save(std::vector &combinations) -{ - GetCombinations(combinations); - Apply(); - - auto AtomicUpdate = [&]() { - ignoreChangedBindings = true; - - obs_hotkey_load_bindings(id, combinations.data(), combinations.size()); - - ignoreChangedBindings = false; - }; - using AtomicUpdate_t = decltype(&AtomicUpdate); - - obs_hotkey_update_atomic([](void *d) { (*static_cast(d))(); }, - static_cast(&AtomicUpdate)); -} - -void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx) -{ - auto edit = new OBSHotkeyEdit(parentWidget(), combo, settings); - edit->setToolTip(toolTip); - - auto revert = new QPushButton; - revert->setProperty("class", "icon-revert"); - revert->setToolTip(QTStr("Revert")); - revert->setEnabled(false); - - auto clear = new QPushButton; - clear->setProperty("class", "icon-clear"); - clear->setToolTip(QTStr("Clear")); - clear->setEnabled(!obs_key_combination_is_empty(combo)); - - QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [=](obs_key_combination_t new_combo) { - clear->setEnabled(!obs_key_combination_is_empty(new_combo)); - revert->setEnabled(edit->original != new_combo); - }); - - auto add = new QPushButton; - add->setProperty("class", "icon-plus"); - add->setToolTip(QTStr("Add")); - - auto remove = new QPushButton; - remove->setProperty("class", "icon-trash"); - remove->setToolTip(QTStr("Remove")); - remove->setEnabled(removeButtons.size() > 0); - - auto CurrentIndex = [&, remove] { - auto res = std::find(begin(removeButtons), end(removeButtons), remove); - return std::distance(begin(removeButtons), res); - }; - - QObject::connect(add, &QPushButton::clicked, [&, CurrentIndex] { - AddEdit({0, OBS_KEY_NONE}, CurrentIndex() + 1); - }); - - QObject::connect(remove, &QPushButton::clicked, [&, CurrentIndex] { RemoveEdit(CurrentIndex()); }); - - QHBoxLayout *subLayout = new QHBoxLayout; - subLayout->setContentsMargins(0, 2, 0, 2); - subLayout->addWidget(edit); - subLayout->addWidget(revert); - subLayout->addWidget(clear); - subLayout->addWidget(add); - subLayout->addWidget(remove); - - if (removeButtons.size() == 1) - removeButtons.front()->setEnabled(true); - - if (idx != -1) { - revertButtons.insert(begin(revertButtons) + idx, revert); - removeButtons.insert(begin(removeButtons) + idx, remove); - edits.insert(begin(edits) + idx, edit); - } else { - revertButtons.emplace_back(revert); - removeButtons.emplace_back(remove); - edits.emplace_back(edit); - } - - layout()->insertLayout(idx, subLayout); - - QObject::connect(revert, &QPushButton::clicked, edit, &OBSHotkeyEdit::ResetKey); - QObject::connect(clear, &QPushButton::clicked, edit, &OBSHotkeyEdit::ClearKey); - - QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [&](obs_key_combination) { emit KeyChanged(); }); - QObject::connect(edit, &OBSHotkeyEdit::SearchKey, [=](obs_key_combination combo) { emit SearchKey(combo); }); -} - -void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal) -{ - auto &edit = *(begin(edits) + idx); - if (!obs_key_combination_is_empty(edit->original) && signal) { - changed = true; - } - - revertButtons.erase(begin(revertButtons) + idx); - removeButtons.erase(begin(removeButtons) + idx); - edits.erase(begin(edits) + idx); - - auto item = layout()->takeAt(static_cast(idx)); - QLayoutItem *child = nullptr; - while ((child = item->layout()->takeAt(0))) { - delete child->widget(); - delete child; - } - delete item; - - if (removeButtons.size() == 1) - removeButtons.front()->setEnabled(false); - - emit KeyChanged(); -} - -void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param) -{ - auto widget = static_cast(data); - auto key = static_cast(calldata_ptr(param, "key")); - - QMetaObject::invokeMethod(widget, "HandleChangedBindings", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); -} - -void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) -{ - if (ignoreChangedBindings || id != id_) - return; - - std::vector bindings; - auto LoadBindings = [&](obs_hotkey_binding_t *binding) { - if (obs_hotkey_binding_get_hotkey_id(binding) != id) - return; - - auto get_combo = obs_hotkey_binding_get_key_combination; - bindings.push_back(get_combo(binding)); - }; - using LoadBindings_t = decltype(&LoadBindings); - - obs_enum_hotkey_bindings( - [](void *data, size_t, obs_hotkey_binding_t *binding) { - auto LoadBindings = *static_cast(data); - LoadBindings(binding); - return true; - }, - static_cast(&LoadBindings)); - - while (edits.size() > 0) - RemoveEdit(edits.size() - 1, false); - - SetKeyCombinations(bindings); -} - -static inline void updateStyle(QWidget *widget) -{ - auto style = widget->style(); - style->unpolish(widget); - style->polish(widget); - widget->update(); -} - -void OBSHotkeyWidget::enterEvent(QEnterEvent *event) -{ - if (!label) - return; - - event->accept(); - label->highlightPair(true); -} - -void OBSHotkeyWidget::leaveEvent(QEvent *event) -{ - if (!label) - return; - - event->accept(); - label->highlightPair(false); -} - -void OBSHotkeyLabel::highlightPair(bool highlight) -{ - if (!pairPartner) - return; - - pairPartner->setProperty("class", highlight ? "text-bright" : ""); - updateStyle(pairPartner); - setProperty("class", highlight ? "text-bright" : ""); - updateStyle(this); -} - -void OBSHotkeyLabel::enterEvent(QEnterEvent *event) -{ - - if (!pairPartner) - return; - - event->accept(); - highlightPair(true); -} - -void OBSHotkeyLabel::leaveEvent(QEvent *event) -{ - if (!pairPartner) - return; - - event->accept(); - highlightPair(false); -} - -void OBSHotkeyLabel::setToolTip(const QString &toolTip) -{ - QLabel::setToolTip(toolTip); - if (widget) - widget->setToolTip(toolTip); -} diff --git a/frontend/settings/OBSHotkeyEdit.hpp b/frontend/settings/OBSHotkeyEdit.hpp index ea7e9b4ae..8514e906c 100644 --- a/frontend/settings/OBSHotkeyEdit.hpp +++ b/frontend/settings/OBSHotkeyEdit.hpp @@ -17,16 +17,13 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include - #include +#include + +class OBSBasicSettings; +class QWidget; + static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) { return c1.modifiers != c2.modifiers || c1.key != c2.key; @@ -37,21 +34,6 @@ static inline bool operator==(const obs_key_combination_t &c1, const obs_key_com return !(c1 != c2); } -class OBSBasicSettings; -class OBSHotkeyWidget; - -class OBSHotkeyLabel : public QLabel { - Q_OBJECT - -public: - QPointer pairPartner; - QPointer widget; - void highlightPair(bool highlight); - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - void setToolTip(const QString &toolTip); -}; - class OBSHotkeyEdit : public QLineEdit { Q_OBJECT; @@ -118,73 +100,3 @@ signals: void KeyChanged(obs_key_combination_t); void SearchKey(obs_key_combination_t); }; - -class OBSHotkeyWidget : public QWidget { - Q_OBJECT; - -public: - OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, - const std::vector &combos = {}) - : QWidget(parent), - id(id), - name(name), - bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", - &OBSHotkeyWidget::BindingsChanged, this), - settings(settings) - { - auto layout = new QVBoxLayout; - layout->setSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); - setLayout(layout); - - SetKeyCombinations(combos); - } - - void SetKeyCombinations(const std::vector &); - - obs_hotkey_id id; - std::string name; - - bool changed = false; - bool Changed() const; - - QPointer label; - std::vector> edits; - - QString toolTip; - void setToolTip(const QString &toolTip_) - { - toolTip = toolTip_; - for (auto &edit : edits) - edit->setToolTip(toolTip_); - } - - void Apply(); - void GetCombinations(std::vector &) const; - void Save(); - void Save(std::vector &combinations); - - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - -private: - void AddEdit(obs_key_combination combo, int idx = -1); - void RemoveEdit(size_t idx, bool signal = true); - - static void BindingsChanged(void *data, calldata_t *param); - - std::vector> removeButtons; - std::vector> revertButtons; - OBSSignal bindingsChanged; - bool ignoreChangedBindings = false; - OBSBasicSettings *settings; - - QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } - -private slots: - void HandleChangedBindings(obs_hotkey_id id_); - -signals: - void KeyChanged(); - void SearchKey(obs_key_combination_t); -}; diff --git a/frontend/settings/OBSHotkeyLabel.cpp b/frontend/settings/OBSHotkeyLabel.cpp index 5dc5355e7..1cfa8e5b0 100644 --- a/frontend/settings/OBSHotkeyLabel.cpp +++ b/frontend/settings/OBSHotkeyLabel.cpp @@ -15,396 +15,13 @@ along with this program. If not, see . ******************************************************************************/ -#include "window-basic-settings.hpp" -#include "moc_hotkey-edit.cpp" +#include "OBSHotkeyLabel.hpp" +#include "OBSHotkeyWidget.hpp" -#include -#include +#include #include -#include -#include -#include "obs-app.hpp" - -void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - obs_key_combination_t new_key; - - switch (event->key()) { - case Qt::Key_Shift: - case Qt::Key_Control: - case Qt::Key_Alt: - case Qt::Key_Meta: - new_key.key = OBS_KEY_NONE; - break; - -#ifdef __APPLE__ - case Qt::Key_CapsLock: - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - break; -#endif - - default: - new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const -{ - if (query == Qt::ImEnabled) { - return false; - } else { - return QLineEdit::inputMethodQuery(query); - } -} - -#ifdef __APPLE__ -void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - if (event->key() != Qt::Key_CapsLock) - return; - - obs_key_combination_t new_key; - - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} -#endif - -void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) -{ - obs_key_combination_t new_key; - - switch (event->button()) { - case Qt::NoButton: - case Qt::LeftButton: - case Qt::RightButton: - case Qt::AllButtons: - case Qt::MouseButtonMask: - return; - - case Qt::MiddleButton: - new_key.key = OBS_KEY_MOUSE3; - break; - -#define MAP_BUTTON(i, j) \ - case Qt::ExtraButton##i: \ - new_key.key = OBS_KEY_MOUSE##j; \ - break; - MAP_BUTTON(1, 4) - MAP_BUTTON(2, 5) - MAP_BUTTON(3, 6) - MAP_BUTTON(4, 7) - MAP_BUTTON(5, 8) - MAP_BUTTON(6, 9) - MAP_BUTTON(7, 10) - MAP_BUTTON(8, 11) - MAP_BUTTON(9, 12) - MAP_BUTTON(10, 13) - MAP_BUTTON(11, 14) - MAP_BUTTON(12, 15) - MAP_BUTTON(13, 16) - MAP_BUTTON(14, 17) - MAP_BUTTON(15, 18) - MAP_BUTTON(16, 19) - MAP_BUTTON(17, 20) - MAP_BUTTON(18, 21) - MAP_BUTTON(19, 22) - MAP_BUTTON(20, 23) - MAP_BUTTON(21, 24) - MAP_BUTTON(22, 25) - MAP_BUTTON(23, 26) - MAP_BUTTON(24, 27) -#undef MAP_BUTTON - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) -{ - if (new_key == key || obs_key_combination_is_empty(new_key)) - return; - - key = new_key; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::RenderKey() -{ - DStr str; - obs_key_combination_to_str(key, str); - - setText(QT_UTF8(str)); -} - -void OBSHotkeyEdit::ResetKey() -{ - key = original; - - changed = false; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::ClearKey() -{ - key = {0, OBS_KEY_NONE}; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::UpdateDuplicationState() -{ - if (!dupeIcon && !hasDuplicate) - return; - - if (!dupeIcon) - CreateDupeIcon(); - - if (dupeIcon->isVisible() != hasDuplicate) { - dupeIcon->setVisible(hasDuplicate); - update(); - } -} - -void OBSHotkeyEdit::InitSignalHandler() -{ - layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", - [](void *this_, calldata_t *) { - auto edit = static_cast(this_); - QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); - }, - this}; -} - -void OBSHotkeyEdit::CreateDupeIcon() -{ - dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); - dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); - QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); - dupeIcon->setVisible(false); -} - -void OBSHotkeyEdit::ReloadKeyLayout() -{ - RenderKey(); -} - -void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) -{ - if (combos.empty()) - AddEdit({0, OBS_KEY_NONE}); - - for (auto combo : combos) - AddEdit(combo); -} - -bool OBSHotkeyWidget::Changed() const -{ - return changed || std::any_of(begin(edits), end(edits), [](OBSHotkeyEdit *edit) { return edit->changed; }); -} - -void OBSHotkeyWidget::Apply() -{ - for (auto &edit : edits) { - edit->original = edit->key; - edit->changed = false; - } - - changed = false; - - for (auto &revertButton : revertButtons) - revertButton->setEnabled(false); -} - -void OBSHotkeyWidget::GetCombinations(std::vector &combinations) const -{ - combinations.clear(); - for (auto &edit : edits) - if (!obs_key_combination_is_empty(edit->key)) - combinations.emplace_back(edit->key); -} - -void OBSHotkeyWidget::Save() -{ - std::vector combinations; - Save(combinations); -} - -void OBSHotkeyWidget::Save(std::vector &combinations) -{ - GetCombinations(combinations); - Apply(); - - auto AtomicUpdate = [&]() { - ignoreChangedBindings = true; - - obs_hotkey_load_bindings(id, combinations.data(), combinations.size()); - - ignoreChangedBindings = false; - }; - using AtomicUpdate_t = decltype(&AtomicUpdate); - - obs_hotkey_update_atomic([](void *d) { (*static_cast(d))(); }, - static_cast(&AtomicUpdate)); -} - -void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx) -{ - auto edit = new OBSHotkeyEdit(parentWidget(), combo, settings); - edit->setToolTip(toolTip); - - auto revert = new QPushButton; - revert->setProperty("class", "icon-revert"); - revert->setToolTip(QTStr("Revert")); - revert->setEnabled(false); - - auto clear = new QPushButton; - clear->setProperty("class", "icon-clear"); - clear->setToolTip(QTStr("Clear")); - clear->setEnabled(!obs_key_combination_is_empty(combo)); - - QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [=](obs_key_combination_t new_combo) { - clear->setEnabled(!obs_key_combination_is_empty(new_combo)); - revert->setEnabled(edit->original != new_combo); - }); - - auto add = new QPushButton; - add->setProperty("class", "icon-plus"); - add->setToolTip(QTStr("Add")); - - auto remove = new QPushButton; - remove->setProperty("class", "icon-trash"); - remove->setToolTip(QTStr("Remove")); - remove->setEnabled(removeButtons.size() > 0); - - auto CurrentIndex = [&, remove] { - auto res = std::find(begin(removeButtons), end(removeButtons), remove); - return std::distance(begin(removeButtons), res); - }; - - QObject::connect(add, &QPushButton::clicked, [&, CurrentIndex] { - AddEdit({0, OBS_KEY_NONE}, CurrentIndex() + 1); - }); - - QObject::connect(remove, &QPushButton::clicked, [&, CurrentIndex] { RemoveEdit(CurrentIndex()); }); - - QHBoxLayout *subLayout = new QHBoxLayout; - subLayout->setContentsMargins(0, 2, 0, 2); - subLayout->addWidget(edit); - subLayout->addWidget(revert); - subLayout->addWidget(clear); - subLayout->addWidget(add); - subLayout->addWidget(remove); - - if (removeButtons.size() == 1) - removeButtons.front()->setEnabled(true); - - if (idx != -1) { - revertButtons.insert(begin(revertButtons) + idx, revert); - removeButtons.insert(begin(removeButtons) + idx, remove); - edits.insert(begin(edits) + idx, edit); - } else { - revertButtons.emplace_back(revert); - removeButtons.emplace_back(remove); - edits.emplace_back(edit); - } - - layout()->insertLayout(idx, subLayout); - - QObject::connect(revert, &QPushButton::clicked, edit, &OBSHotkeyEdit::ResetKey); - QObject::connect(clear, &QPushButton::clicked, edit, &OBSHotkeyEdit::ClearKey); - - QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [&](obs_key_combination) { emit KeyChanged(); }); - QObject::connect(edit, &OBSHotkeyEdit::SearchKey, [=](obs_key_combination combo) { emit SearchKey(combo); }); -} - -void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal) -{ - auto &edit = *(begin(edits) + idx); - if (!obs_key_combination_is_empty(edit->original) && signal) { - changed = true; - } - - revertButtons.erase(begin(revertButtons) + idx); - removeButtons.erase(begin(removeButtons) + idx); - edits.erase(begin(edits) + idx); - - auto item = layout()->takeAt(static_cast(idx)); - QLayoutItem *child = nullptr; - while ((child = item->layout()->takeAt(0))) { - delete child->widget(); - delete child; - } - delete item; - - if (removeButtons.size() == 1) - removeButtons.front()->setEnabled(false); - - emit KeyChanged(); -} - -void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param) -{ - auto widget = static_cast(data); - auto key = static_cast(calldata_ptr(param, "key")); - - QMetaObject::invokeMethod(widget, "HandleChangedBindings", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); -} - -void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) -{ - if (ignoreChangedBindings || id != id_) - return; - - std::vector bindings; - auto LoadBindings = [&](obs_hotkey_binding_t *binding) { - if (obs_hotkey_binding_get_hotkey_id(binding) != id) - return; - - auto get_combo = obs_hotkey_binding_get_key_combination; - bindings.push_back(get_combo(binding)); - }; - using LoadBindings_t = decltype(&LoadBindings); - - obs_enum_hotkey_bindings( - [](void *data, size_t, obs_hotkey_binding_t *binding) { - auto LoadBindings = *static_cast(data); - LoadBindings(binding); - return true; - }, - static_cast(&LoadBindings)); - - while (edits.size() > 0) - RemoveEdit(edits.size() - 1, false); - - SetKeyCombinations(bindings); -} +#include "moc_OBSHotkeyLabel.cpp" static inline void updateStyle(QWidget *widget) { @@ -414,24 +31,6 @@ static inline void updateStyle(QWidget *widget) widget->update(); } -void OBSHotkeyWidget::enterEvent(QEnterEvent *event) -{ - if (!label) - return; - - event->accept(); - label->highlightPair(true); -} - -void OBSHotkeyWidget::leaveEvent(QEvent *event) -{ - if (!label) - return; - - event->accept(); - label->highlightPair(false); -} - void OBSHotkeyLabel::highlightPair(bool highlight) { if (!pairPartner) diff --git a/frontend/settings/OBSHotkeyLabel.hpp b/frontend/settings/OBSHotkeyLabel.hpp index ea7e9b4ae..789453a18 100644 --- a/frontend/settings/OBSHotkeyLabel.hpp +++ b/frontend/settings/OBSHotkeyLabel.hpp @@ -17,27 +17,9 @@ #pragma once -#include -#include -#include -#include -#include -#include #include +#include -#include - -static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) -{ - return c1.modifiers != c2.modifiers || c1.key != c2.key; -} - -static inline bool operator==(const obs_key_combination_t &c1, const obs_key_combination_t &c2) -{ - return !(c1 != c2); -} - -class OBSBasicSettings; class OBSHotkeyWidget; class OBSHotkeyLabel : public QLabel { @@ -51,140 +33,3 @@ public: void leaveEvent(QEvent *event) override; void setToolTip(const QString &toolTip); }; - -class OBSHotkeyEdit : public QLineEdit { - Q_OBJECT; - -public: - OBSHotkeyEdit(QWidget *parent, obs_key_combination_t original, OBSBasicSettings *settings) - : QLineEdit(parent), - original(original), - settings(settings) - { -#ifdef __APPLE__ - // disable the input cursor on OSX, focus should be clear - // enough with the default focus frame - setReadOnly(true); -#endif - setAttribute(Qt::WA_InputMethodEnabled, false); - setAttribute(Qt::WA_MacShowFocusRect, true); - InitSignalHandler(); - ResetKey(); - } - OBSHotkeyEdit(QWidget *parent = nullptr) : QLineEdit(parent), original({}), settings(nullptr) - { -#ifdef __APPLE__ - // disable the input cursor on OSX, focus should be clear - // enough with the default focus frame - setReadOnly(true); -#endif - setAttribute(Qt::WA_InputMethodEnabled, false); - setAttribute(Qt::WA_MacShowFocusRect, true); - InitSignalHandler(); - ResetKey(); - } - - obs_key_combination_t original; - obs_key_combination_t key; - OBSBasicSettings *settings; - bool changed = false; - - void UpdateDuplicationState(); - bool hasDuplicate = false; - QVariant inputMethodQuery(Qt::InputMethodQuery) const override; - -protected: - OBSSignal layoutChanged; - QAction *dupeIcon = nullptr; - - void InitSignalHandler(); - void CreateDupeIcon(); - - void keyPressEvent(QKeyEvent *event) override; -#ifdef __APPLE__ - void keyReleaseEvent(QKeyEvent *event) override; -#endif - void mousePressEvent(QMouseEvent *event) override; - - void RenderKey(); - -public slots: - void HandleNewKey(obs_key_combination_t new_key); - void ReloadKeyLayout(); - void ResetKey(); - void ClearKey(); - -signals: - void KeyChanged(obs_key_combination_t); - void SearchKey(obs_key_combination_t); -}; - -class OBSHotkeyWidget : public QWidget { - Q_OBJECT; - -public: - OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, - const std::vector &combos = {}) - : QWidget(parent), - id(id), - name(name), - bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", - &OBSHotkeyWidget::BindingsChanged, this), - settings(settings) - { - auto layout = new QVBoxLayout; - layout->setSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); - setLayout(layout); - - SetKeyCombinations(combos); - } - - void SetKeyCombinations(const std::vector &); - - obs_hotkey_id id; - std::string name; - - bool changed = false; - bool Changed() const; - - QPointer label; - std::vector> edits; - - QString toolTip; - void setToolTip(const QString &toolTip_) - { - toolTip = toolTip_; - for (auto &edit : edits) - edit->setToolTip(toolTip_); - } - - void Apply(); - void GetCombinations(std::vector &) const; - void Save(); - void Save(std::vector &combinations); - - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - -private: - void AddEdit(obs_key_combination combo, int idx = -1); - void RemoveEdit(size_t idx, bool signal = true); - - static void BindingsChanged(void *data, calldata_t *param); - - std::vector> removeButtons; - std::vector> revertButtons; - OBSSignal bindingsChanged; - bool ignoreChangedBindings = false; - OBSBasicSettings *settings; - - QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } - -private slots: - void HandleChangedBindings(obs_hotkey_id id_); - -signals: - void KeyChanged(); - void SearchKey(obs_key_combination_t); -}; diff --git a/frontend/settings/OBSHotkeyWidget.cpp b/frontend/settings/OBSHotkeyWidget.cpp index 5dc5355e7..77d691c81 100644 --- a/frontend/settings/OBSHotkeyWidget.cpp +++ b/frontend/settings/OBSHotkeyWidget.cpp @@ -1,219 +1,28 @@ /****************************************************************************** - Copyright (C) 2014-2015 by Ruwen Hahn + Copyright (C) 2014-2015 by Ruwen Hahn + + 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 . + ******************************************************************************/ - 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. +#include "OBSHotkeyWidget.hpp" +#include "OBSHotkeyLabel.hpp" - 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. +#include - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ +#include -#include "window-basic-settings.hpp" -#include "moc_hotkey-edit.cpp" - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" - -void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - obs_key_combination_t new_key; - - switch (event->key()) { - case Qt::Key_Shift: - case Qt::Key_Control: - case Qt::Key_Alt: - case Qt::Key_Meta: - new_key.key = OBS_KEY_NONE; - break; - -#ifdef __APPLE__ - case Qt::Key_CapsLock: - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - break; -#endif - - default: - new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const -{ - if (query == Qt::ImEnabled) { - return false; - } else { - return QLineEdit::inputMethodQuery(query); - } -} - -#ifdef __APPLE__ -void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - if (event->key() != Qt::Key_CapsLock) - return; - - obs_key_combination_t new_key; - - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} -#endif - -void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) -{ - obs_key_combination_t new_key; - - switch (event->button()) { - case Qt::NoButton: - case Qt::LeftButton: - case Qt::RightButton: - case Qt::AllButtons: - case Qt::MouseButtonMask: - return; - - case Qt::MiddleButton: - new_key.key = OBS_KEY_MOUSE3; - break; - -#define MAP_BUTTON(i, j) \ - case Qt::ExtraButton##i: \ - new_key.key = OBS_KEY_MOUSE##j; \ - break; - MAP_BUTTON(1, 4) - MAP_BUTTON(2, 5) - MAP_BUTTON(3, 6) - MAP_BUTTON(4, 7) - MAP_BUTTON(5, 8) - MAP_BUTTON(6, 9) - MAP_BUTTON(7, 10) - MAP_BUTTON(8, 11) - MAP_BUTTON(9, 12) - MAP_BUTTON(10, 13) - MAP_BUTTON(11, 14) - MAP_BUTTON(12, 15) - MAP_BUTTON(13, 16) - MAP_BUTTON(14, 17) - MAP_BUTTON(15, 18) - MAP_BUTTON(16, 19) - MAP_BUTTON(17, 20) - MAP_BUTTON(18, 21) - MAP_BUTTON(19, 22) - MAP_BUTTON(20, 23) - MAP_BUTTON(21, 24) - MAP_BUTTON(22, 25) - MAP_BUTTON(23, 26) - MAP_BUTTON(24, 27) -#undef MAP_BUTTON - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) -{ - if (new_key == key || obs_key_combination_is_empty(new_key)) - return; - - key = new_key; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::RenderKey() -{ - DStr str; - obs_key_combination_to_str(key, str); - - setText(QT_UTF8(str)); -} - -void OBSHotkeyEdit::ResetKey() -{ - key = original; - - changed = false; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::ClearKey() -{ - key = {0, OBS_KEY_NONE}; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::UpdateDuplicationState() -{ - if (!dupeIcon && !hasDuplicate) - return; - - if (!dupeIcon) - CreateDupeIcon(); - - if (dupeIcon->isVisible() != hasDuplicate) { - dupeIcon->setVisible(hasDuplicate); - update(); - } -} - -void OBSHotkeyEdit::InitSignalHandler() -{ - layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", - [](void *this_, calldata_t *) { - auto edit = static_cast(this_); - QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); - }, - this}; -} - -void OBSHotkeyEdit::CreateDupeIcon() -{ - dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); - dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); - QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); - dupeIcon->setVisible(false); -} - -void OBSHotkeyEdit::ReloadKeyLayout() -{ - RenderKey(); -} +#include "moc_OBSHotkeyWidget.cpp" void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) { @@ -406,14 +215,6 @@ void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) SetKeyCombinations(bindings); } -static inline void updateStyle(QWidget *widget) -{ - auto style = widget->style(); - style->unpolish(widget); - style->polish(widget); - widget->update(); -} - void OBSHotkeyWidget::enterEvent(QEnterEvent *event) { if (!label) @@ -431,40 +232,3 @@ void OBSHotkeyWidget::leaveEvent(QEvent *event) event->accept(); label->highlightPair(false); } - -void OBSHotkeyLabel::highlightPair(bool highlight) -{ - if (!pairPartner) - return; - - pairPartner->setProperty("class", highlight ? "text-bright" : ""); - updateStyle(pairPartner); - setProperty("class", highlight ? "text-bright" : ""); - updateStyle(this); -} - -void OBSHotkeyLabel::enterEvent(QEnterEvent *event) -{ - - if (!pairPartner) - return; - - event->accept(); - highlightPair(true); -} - -void OBSHotkeyLabel::leaveEvent(QEvent *event) -{ - if (!pairPartner) - return; - - event->accept(); - highlightPair(false); -} - -void OBSHotkeyLabel::setToolTip(const QString &toolTip) -{ - QLabel::setToolTip(toolTip); - if (widget) - widget->setToolTip(toolTip); -} diff --git a/frontend/settings/OBSHotkeyWidget.hpp b/frontend/settings/OBSHotkeyWidget.hpp index ea7e9b4ae..12f15f282 100644 --- a/frontend/settings/OBSHotkeyWidget.hpp +++ b/frontend/settings/OBSHotkeyWidget.hpp @@ -17,107 +17,15 @@ #pragma once -#include -#include +#include "OBSHotkeyEdit.hpp" + +#include #include #include #include -#include -#include - -#include - -static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) -{ - return c1.modifiers != c2.modifiers || c1.key != c2.key; -} - -static inline bool operator==(const obs_key_combination_t &c1, const obs_key_combination_t &c2) -{ - return !(c1 != c2); -} class OBSBasicSettings; -class OBSHotkeyWidget; - -class OBSHotkeyLabel : public QLabel { - Q_OBJECT - -public: - QPointer pairPartner; - QPointer widget; - void highlightPair(bool highlight); - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - void setToolTip(const QString &toolTip); -}; - -class OBSHotkeyEdit : public QLineEdit { - Q_OBJECT; - -public: - OBSHotkeyEdit(QWidget *parent, obs_key_combination_t original, OBSBasicSettings *settings) - : QLineEdit(parent), - original(original), - settings(settings) - { -#ifdef __APPLE__ - // disable the input cursor on OSX, focus should be clear - // enough with the default focus frame - setReadOnly(true); -#endif - setAttribute(Qt::WA_InputMethodEnabled, false); - setAttribute(Qt::WA_MacShowFocusRect, true); - InitSignalHandler(); - ResetKey(); - } - OBSHotkeyEdit(QWidget *parent = nullptr) : QLineEdit(parent), original({}), settings(nullptr) - { -#ifdef __APPLE__ - // disable the input cursor on OSX, focus should be clear - // enough with the default focus frame - setReadOnly(true); -#endif - setAttribute(Qt::WA_InputMethodEnabled, false); - setAttribute(Qt::WA_MacShowFocusRect, true); - InitSignalHandler(); - ResetKey(); - } - - obs_key_combination_t original; - obs_key_combination_t key; - OBSBasicSettings *settings; - bool changed = false; - - void UpdateDuplicationState(); - bool hasDuplicate = false; - QVariant inputMethodQuery(Qt::InputMethodQuery) const override; - -protected: - OBSSignal layoutChanged; - QAction *dupeIcon = nullptr; - - void InitSignalHandler(); - void CreateDupeIcon(); - - void keyPressEvent(QKeyEvent *event) override; -#ifdef __APPLE__ - void keyReleaseEvent(QKeyEvent *event) override; -#endif - void mousePressEvent(QMouseEvent *event) override; - - void RenderKey(); - -public slots: - void HandleNewKey(obs_key_combination_t new_key); - void ReloadKeyLayout(); - void ResetKey(); - void ClearKey(); - -signals: - void KeyChanged(obs_key_combination_t); - void SearchKey(obs_key_combination_t); -}; +class OBSHotkeyLabel; class OBSHotkeyWidget : public QWidget { Q_OBJECT; diff --git a/frontend/utility/SettingsEventFilter.hpp b/frontend/utility/SettingsEventFilter.hpp index d8a35501b..d8ffa8061 100644 --- a/frontend/utility/SettingsEventFilter.hpp +++ b/frontend/utility/SettingsEventFilter.hpp @@ -16,47 +16,13 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#pragma once -#include "audio-encoders.hpp" -#include "hotkey-edit.hpp" -#include "source-label.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "properties-view.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-settings.cpp" -#include "window-basic-main-outputs.hpp" -#include "window-projector.hpp" +#include +#include -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -#include -#include -#include "ui-config.h" - -using namespace std; +#include +#include class SettingsEventFilter : public QObject { QScopedPointer shortcutFilter; @@ -83,5743 +49,3 @@ protected: return shortcutFilter->filter(obj, event); } }; - -static inline bool ResTooHigh(uint32_t cx, uint32_t cy) -{ - return cx > 16384 || cy > 16384; -} - -static inline bool ResTooLow(uint32_t cx, uint32_t cy) -{ - return cx < 32 || cy < 32; -} - -/* parses "[width]x[height]", string, i.e. 1024x768 */ -static bool ConvertResText(const char *res, uint32_t &cx, uint32_t &cy) -{ - BaseLexer lex; - base_token token; - - lexer_start(lex, res); - - /* parse width */ - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != BASETOKEN_DIGIT) - return false; - - cx = std::stoul(token.text.array); - - /* parse 'x' */ - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (strref_cmpi(&token.text, "x") != 0) - return false; - - /* parse height */ - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != BASETOKEN_DIGIT) - return false; - - cy = std::stoul(token.text.array); - - /* shouldn't be any more tokens after this */ - if (lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - - if (ResTooHigh(cx, cy) || ResTooLow(cx, cy)) { - cx = cy = 0; - return false; - } - - return true; -} - -static inline bool WidgetChanged(QWidget *widget) -{ - return widget->property("changed").toBool(); -} - -static inline void SetComboByName(QComboBox *combo, const char *name) -{ - int idx = combo->findText(QT_UTF8(name)); - if (idx != -1) - combo->setCurrentIndex(idx); -} - -static inline bool SetComboByValue(QComboBox *combo, const char *name) -{ - int idx = combo->findData(QT_UTF8(name)); - if (idx != -1) { - combo->setCurrentIndex(idx); - return true; - } - - return false; -} - -static inline bool SetInvalidValue(QComboBox *combo, const char *name, const char *data = nullptr) -{ - combo->insertItem(0, name, data); - - QStandardItemModel *model = dynamic_cast(combo->model()); - if (!model) - return false; - - QStandardItem *item = model->item(0); - item->setFlags(Qt::NoItemFlags); - - combo->setCurrentIndex(0); - return true; -} - -static inline QString GetComboData(QComboBox *combo) -{ - int idx = combo->currentIndex(); - if (idx == -1) - return QString(); - - return combo->itemData(idx).toString(); -} - -static int FindEncoder(QComboBox *combo, const char *name, int id) -{ - FFmpegCodec codec{name, id}; - - for (int i = 0; i < combo->count(); i++) { - QVariant v = combo->itemData(i); - if (!v.isNull()) { - if (codec == v.value()) { - return i; - } - } - } - return -1; -} - -#define INVALID_BITRATE 10000 -static int FindClosestAvailableAudioBitrate(QComboBox *box, int bitrate) -{ - QList bitrates; - int prev = 0; - int next = INVALID_BITRATE; - - for (int i = 0; i < box->count(); i++) - bitrates << box->itemText(i).toInt(); - - for (int val : bitrates) { - if (next > val) { - if (val == bitrate) - return bitrate; - - if (val < next && val > bitrate) - next = val; - if (val > prev && val < bitrate) - prev = val; - } - } - - if (next != INVALID_BITRATE) - return next; - if (prev != 0) - return prev; - return 192; -} -#undef INVALID_BITRATE - -static void PopulateSimpleBitrates(QComboBox *box, bool opus) -{ - auto &bitrateMap = opus ? GetSimpleOpusEncoderBitrateMap() : GetSimpleAACEncoderBitrateMap(); - if (bitrateMap.empty()) - return; - - vector> pairs; - for (auto &entry : bitrateMap) - pairs.emplace_back(QString::number(entry.first), obs_encoder_get_display_name(entry.second.c_str())); - - QString currentBitrate = box->currentText(); - box->clear(); - - for (auto &pair : pairs) { - box->addItem(pair.first); - box->setItemData(box->count() - 1, pair.second, Qt::ToolTipRole); - } - - if (box->findData(currentBitrate) == -1) { - int bitrate = FindClosestAvailableAudioBitrate(box, currentBitrate.toInt()); - box->setCurrentText(QString::number(bitrate)); - } else - box->setCurrentText(currentBitrate); -} - -static void PopulateAdvancedBitrates(initializer_list boxes, const char *stream_id, const char *rec_id) -{ - auto &streamBitrates = GetAudioEncoderBitrates(stream_id); - auto &recBitrates = GetAudioEncoderBitrates(rec_id); - if (streamBitrates.empty() || recBitrates.empty()) - return; - - QList streamBitratesList; - for (auto &bitrate : streamBitrates) - streamBitratesList << bitrate; - - for (auto box : boxes) { - QString currentBitrate = box->currentText(); - box->clear(); - - for (auto &bitrate : recBitrates) { - if (streamBitratesList.indexOf(bitrate) == -1) - continue; - - box->addItem(QString::number(bitrate)); - } - - if (box->findData(currentBitrate) == -1) { - int bitrate = FindClosestAvailableAudioBitrate(box, currentBitrate.toInt()); - box->setCurrentText(QString::number(bitrate)); - } else - box->setCurrentText(currentBitrate); - } -} - -static std::tuple aspect_ratio(int cx, int cy) -{ - int common = std::gcd(cx, cy); - int newCX = cx / common; - int newCY = cy / common; - - if (newCX == 8 && newCY == 5) { - newCX = 16; - newCY = 10; - } - - return std::make_tuple(newCX, newCY); -} - -static inline void HighlightGroupBoxLabel(QGroupBox *gb, QWidget *widget, QString objectName) -{ - QFormLayout *layout = qobject_cast(gb->layout()); - - if (!layout) - return; - - QLabel *label = qobject_cast(layout->labelForField(widget)); - - if (label) { - label->setObjectName(objectName); - - label->style()->unpolish(label); - label->style()->polish(label); - } -} - -void RestrictResetBitrates(initializer_list boxes, int maxbitrate); - -/* clang-format off */ -#define COMBO_CHANGED &QComboBox::currentIndexChanged -#define EDIT_CHANGED &QLineEdit::textChanged -#define CBEDIT_CHANGED &QComboBox::editTextChanged -#define CHECK_CHANGED &QCheckBox::toggled -#define GROUP_CHANGED &QGroupBox::toggled -#define SCROLL_CHANGED &QSpinBox::valueChanged -#define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged -#define TEXT_CHANGED &QPlainTextEdit::textChanged - -#define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged -#define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed -#define OUTPUTS_CHANGED &OBSBasicSettings::OutputsChanged -#define AUDIO_RESTART &OBSBasicSettings::AudioChangedRestart -#define AUDIO_CHANGED &OBSBasicSettings::AudioChanged -#define VIDEO_RES &OBSBasicSettings::VideoChangedResolution -#define VIDEO_CHANGED &OBSBasicSettings::VideoChanged -#define A11Y_CHANGED &OBSBasicSettings::A11yChanged -#define APPEAR_CHANGED &OBSBasicSettings::AppearanceChanged -#define ADV_CHANGED &OBSBasicSettings::AdvancedChanged -#define ADV_RESTART &OBSBasicSettings::AdvancedChangedRestart -/* clang-format on */ - -OBSBasicSettings::OBSBasicSettings(QWidget *parent) - : QDialog(parent), - main(qobject_cast(parent)), - ui(new Ui::OBSBasicSettings) -{ - string path; - - EnableThreadedMessageBoxes(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - main->EnableOutputs(false); - - ui->listWidget->setAttribute(Qt::WA_MacShowFocusRect, false); - - /* clang-format off */ - HookWidget(ui->language, COMBO_CHANGED, GENERAL_CHANGED); - HookWidget(ui->updateChannelBox, COMBO_CHANGED, GENERAL_CHANGED); - HookWidget(ui->enableAutoUpdates, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->openStatsOnStartup, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->hideOBSFromCapture, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->warnBeforeStreamStart,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->warnBeforeStreamStop, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->warnBeforeRecordStop, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->hideProjectorCursor, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->projectorAlwaysOnTop, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->recordWhenStreaming, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->keepRecordStreamStops,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->replayWhileStreaming, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->keepReplayStreamStops,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->systemTrayEnabled, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->systemTrayWhenStarted,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->systemTrayAlways, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->saveProjectors, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->closeProjectors, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->snappingEnabled, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->screenSnapping, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->centerSnapping, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->sourceSnapping, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->snapDistance, DSCROLL_CHANGED,GENERAL_CHANGED); - HookWidget(ui->overflowHide, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->overflowAlwaysVisible,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->overflowSelectionHide,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->previewSafeAreas, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->automaticSearch, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->previewSpacingHelpers,CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->doubleClickSwitch, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->studioPortraitLayout, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->prevProgLabelToggle, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->multiviewMouseSwitch, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->multiviewDrawNames, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->multiviewDrawAreas, CHECK_CHANGED, GENERAL_CHANGED); - HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); - HookWidget(ui->theme, COMBO_CHANGED, APPEAR_CHANGED); - HookWidget(ui->themeVariant, COMBO_CHANGED, APPEAR_CHANGED); - HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->serviceCustomServer, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoMaximumVideoTracksAuto, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoMaximumVideoTracks, SCROLL_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoStreamDumpEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoConfigOverrideEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->multitrackVideoConfigOverride, TEXT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutputVBitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutStrEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutStrAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutputABitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutAdvanced, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutPreset, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutCustom, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecQuality, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutRecTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleReplayBuf, GROUP_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->simpleRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRescaleFilter, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMultiTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecType, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecPath, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecRescaleFilter, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutSplitFile, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutSplitFileType, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutSplitFileTime, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutSplitFileSize, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutRecTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->flvTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFType, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFRecPath, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFURL, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFFormat, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFMCfg, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFVBitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFVGOPSize, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFUseRescale, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFIgnoreCompat, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFVEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFVCfg, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFABitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack4, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack5, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFTrack6, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutFFACfg, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack1Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack1Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack2Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack2Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack3Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack3Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack4Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack4Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack5Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack5Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack6Bitrate, COMBO_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advOutTrack6Name, EDIT_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advReplayBuf, CHECK_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->advRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); - HookWidget(ui->channelSetup, COMBO_CHANGED, AUDIO_RESTART); - HookWidget(ui->sampleRate, COMBO_CHANGED, AUDIO_RESTART); - HookWidget(ui->meterDecayRate, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->peakMeterType, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->desktopAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->desktopAudioDevice2, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->auxAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->auxAudioDevice2, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->auxAudioDevice3, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->auxAudioDevice4, COMBO_CHANGED, AUDIO_CHANGED); - HookWidget(ui->baseResolution, CBEDIT_CHANGED, VIDEO_RES); - HookWidget(ui->outputResolution, CBEDIT_CHANGED, VIDEO_RES); - HookWidget(ui->downscaleFilter, COMBO_CHANGED, VIDEO_CHANGED); - HookWidget(ui->fpsType, COMBO_CHANGED, VIDEO_CHANGED); - HookWidget(ui->fpsCommon, COMBO_CHANGED, VIDEO_CHANGED); - HookWidget(ui->fpsInteger, SCROLL_CHANGED, VIDEO_CHANGED); - HookWidget(ui->fpsNumerator, SCROLL_CHANGED, VIDEO_CHANGED); - HookWidget(ui->fpsDenominator, SCROLL_CHANGED, VIDEO_CHANGED); - HookWidget(ui->colorsGroupBox, GROUP_CHANGED, A11Y_CHANGED); - HookWidget(ui->colorPreset, COMBO_CHANGED, A11Y_CHANGED); - HookWidget(ui->renderer, COMBO_CHANGED, ADV_RESTART); - HookWidget(ui->adapter, COMBO_CHANGED, ADV_RESTART); - HookWidget(ui->colorFormat, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->colorSpace, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->sdrWhiteLevel, SCROLL_CHANGED, ADV_CHANGED); - HookWidget(ui->hdrNominalPeakLevel, SCROLL_CHANGED, ADV_CHANGED); - HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED); - if (obs_audio_monitoring_available()) - HookWidget(ui->monitoringDevice, COMBO_CHANGED, ADV_CHANGED); -#ifdef _WIN32 - HookWidget(ui->disableAudioDucking, CHECK_CHANGED, ADV_CHANGED); -#endif -#if defined(_WIN32) || defined(__APPLE__) - HookWidget(ui->browserHWAccel, CHECK_CHANGED, ADV_RESTART); -#endif - HookWidget(ui->filenameFormatting, EDIT_CHANGED, ADV_CHANGED); - HookWidget(ui->overwriteIfExists, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->simpleRBPrefix, EDIT_CHANGED, ADV_CHANGED); - HookWidget(ui->simpleRBSuffix, EDIT_CHANGED, ADV_CHANGED); - HookWidget(ui->streamDelayEnable, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->streamDelaySec, SCROLL_CHANGED, ADV_CHANGED); - HookWidget(ui->streamDelayPreserve, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->reconnectEnable, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->reconnectRetryDelay, SCROLL_CHANGED, ADV_CHANGED); - HookWidget(ui->reconnectMaxRetries, SCROLL_CHANGED, ADV_CHANGED); - HookWidget(ui->processPriority, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->confirmOnExit, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->bindToIP, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->ipFamily, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->enableNewSocketLoop, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->enableLowLatencyMode, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->hotkeyFocusType, COMBO_CHANGED, ADV_CHANGED); - HookWidget(ui->autoRemux, CHECK_CHANGED, ADV_CHANGED); - HookWidget(ui->dynBitrate, CHECK_CHANGED, ADV_CHANGED); - /* clang-format on */ - -#define ADD_HOTKEY_FOCUS_TYPE(s) ui->hotkeyFocusType->addItem(QTStr("Basic.Settings.Advanced.Hotkeys." s), s) - - ADD_HOTKEY_FOCUS_TYPE("NeverDisableHotkeys"); - ADD_HOTKEY_FOCUS_TYPE("DisableHotkeysInFocus"); - ADD_HOTKEY_FOCUS_TYPE("DisableHotkeysOutOfFocus"); - -#undef ADD_HOTKEY_FOCUS_TYPE - - ui->simpleOutputVBitrate->setSingleStep(50); - ui->simpleOutputVBitrate->setSuffix(" Kbps"); - ui->advOutFFVBitrate->setSingleStep(50); - ui->advOutFFVBitrate->setSuffix(" Kbps"); - ui->advOutFFABitrate->setSuffix(" Kbps"); - -#if !defined(_WIN32) && !defined(ENABLE_SPARKLE_UPDATER) - delete ui->updateSettingsGroupBox; - ui->updateSettingsGroupBox = nullptr; - ui->updateChannelLabel = nullptr; - ui->updateChannelBox = nullptr; - ui->enableAutoUpdates = nullptr; -#else - // Hide update section if disabled - if (App()->IsUpdaterDisabled()) - ui->updateSettingsGroupBox->hide(); -#endif - - // Remove the Advanced Audio section if monitoring is not supported, as the monitoring device selection is the only item in the group box. - if (!obs_audio_monitoring_available()) { - delete ui->monitoringDeviceLabel; - ui->monitoringDeviceLabel = nullptr; - delete ui->monitoringDevice; - ui->monitoringDevice = nullptr; - } - -#ifdef _WIN32 - if (!SetDisplayAffinitySupported()) { - delete ui->hideOBSFromCapture; - ui->hideOBSFromCapture = nullptr; - } - - static struct ProcessPriority { - const char *name; - const char *val; - } processPriorities[] = { - {"Basic.Settings.Advanced.General.ProcessPriority.High", "High"}, - {"Basic.Settings.Advanced.General.ProcessPriority.AboveNormal", "AboveNormal"}, - {"Basic.Settings.Advanced.General.ProcessPriority.Normal", "Normal"}, - {"Basic.Settings.Advanced.General.ProcessPriority.BelowNormal", "BelowNormal"}, - {"Basic.Settings.Advanced.General.ProcessPriority.Idle", "Idle"}, - }; - - for (ProcessPriority pri : processPriorities) - ui->processPriority->addItem(QTStr(pri.name), pri.val); - -#else - delete ui->rendererLabel; - delete ui->renderer; - delete ui->adapterLabel; - delete ui->adapter; - delete ui->processPriorityLabel; - delete ui->processPriority; - delete ui->enableNewSocketLoop; - delete ui->enableLowLatencyMode; - delete ui->hideOBSFromCapture; -#ifdef __linux__ - delete ui->browserHWAccel; - delete ui->sourcesGroup; -#endif - delete ui->disableAudioDucking; - - ui->rendererLabel = nullptr; - ui->renderer = nullptr; - ui->adapterLabel = nullptr; - ui->adapter = nullptr; - ui->processPriorityLabel = nullptr; - ui->processPriority = nullptr; - ui->enableNewSocketLoop = nullptr; - ui->enableLowLatencyMode = nullptr; - ui->hideOBSFromCapture = nullptr; -#ifdef __linux__ - ui->browserHWAccel = nullptr; - ui->sourcesGroup = nullptr; -#endif - ui->disableAudioDucking = nullptr; -#endif - -#ifndef __APPLE__ - delete ui->disableOSXVSync; - delete ui->resetOSXVSync; - ui->disableOSXVSync = nullptr; - ui->resetOSXVSync = nullptr; -#endif - - connect(ui->streamDelaySec, &QSpinBox::valueChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->outputMode, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack1Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack2Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack3Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack4Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack5Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(ui->advOutTrack6Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::UpdateStreamDelayEstimate); - - //Apply button disabled until change. - EnableApplyButton(false); - - installEventFilter(new SettingsEventFilter()); - - LoadColorRanges(); - LoadColorSpaces(); - LoadColorFormats(); - LoadFormats(); - - auto ReloadAudioSources = [](void *data, calldata_t *param) { - auto settings = static_cast(data); - auto source = static_cast(calldata_ptr(param, "source")); - - if (!source) - return; - - if (!(obs_source_get_output_flags(source) & OBS_SOURCE_AUDIO)) - return; - - QMetaObject::invokeMethod(settings, "ReloadAudioSources", Qt::QueuedConnection); - }; - sourceCreated.Connect(obs_get_signal_handler(), "source_create", ReloadAudioSources, this); - channelChanged.Connect(obs_get_signal_handler(), "channel_change", ReloadAudioSources, this); - - hotkeyConflictIcon = QIcon::fromTheme("obs", QIcon(":/res/images/warning.svg")); - - auto ReloadHotkeys = [](void *data, calldata_t *) { - auto settings = static_cast(data); - QMetaObject::invokeMethod(settings, "ReloadHotkeys"); - }; - hotkeyRegistered.Connect(obs_get_signal_handler(), "hotkey_register", ReloadHotkeys, this); - - auto ReloadHotkeysIgnore = [](void *data, calldata_t *param) { - auto settings = static_cast(data); - auto key = static_cast(calldata_ptr(param, "key")); - QMetaObject::invokeMethod(settings, "ReloadHotkeys", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); - }; - hotkeyUnregistered.Connect(obs_get_signal_handler(), "hotkey_unregister", ReloadHotkeysIgnore, this); - - FillSimpleRecordingValues(); - if (obs_audio_monitoring_available()) - FillAudioMonitoringDevices(); - - connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SurroundWarning); - connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SpeakerLayoutChanged); - connect(ui->lowLatencyBuffering, &QCheckBox::clicked, this, &OBSBasicSettings::LowLatencyBufferingChanged); - connect(ui->simpleOutRecQuality, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingQualityChanged); - connect(ui->simpleOutRecQuality, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingQualityLosslessWarning); - connect(ui->simpleOutRecFormat, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutStrEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleStreamingEncoderChanged); - connect(ui->simpleOutStrEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutRecEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutRecAEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleOutAdvanced, &QCheckBox::toggled, this, &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->ignoreRecommended, &QCheckBox::toggled, this, &OBSBasicSettings::SimpleRecordingEncoderChanged); - connect(ui->simpleReplayBuf, &QGroupBox::toggled, this, &OBSBasicSettings::SimpleReplayBufferChanged); - connect(ui->simpleOutputVBitrate, &QSpinBox::valueChanged, this, &OBSBasicSettings::SimpleReplayBufferChanged); - connect(ui->simpleOutputABitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleReplayBufferChanged); - connect(ui->simpleRBSecMax, &QSpinBox::valueChanged, this, &OBSBasicSettings::SimpleReplayBufferChanged); -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - connect(ui->advOutSplitFile, &QCheckBox::checkStateChanged, this, &OBSBasicSettings::AdvOutSplitFileChanged); -#else - connect(ui->advOutSplitFile, &QCheckBox::stateChanged, this, &OBSBasicSettings::AdvOutSplitFileChanged); -#endif - connect(ui->advOutSplitFileType, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvOutSplitFileChanged); - connect(ui->advReplayBuf, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack1, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack2, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack3, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack4, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack5, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecTrack6, &QCheckBox::toggled, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack1Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack2Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack3Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack4Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack5Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutTrack6Bitrate, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecType, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advOutRecEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); - connect(ui->advRBSecMax, &QSpinBox::valueChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); - - // GPU scaling filters - auto addScaleFilter = [&](const char *string, int value) -> void { - ui->advOutRescaleFilter->addItem(QTStr(string), value); - ui->advOutRecRescaleFilter->addItem(QTStr(string), value); - }; - - addScaleFilter("Basic.Settings.Output.Adv.Rescale.Disabled", OBS_SCALE_DISABLE); - addScaleFilter("Basic.Settings.Video.DownscaleFilter.Bilinear", OBS_SCALE_BILINEAR); - addScaleFilter("Basic.Settings.Video.DownscaleFilter.Area", OBS_SCALE_AREA); - addScaleFilter("Basic.Settings.Video.DownscaleFilter.Bicubic", OBS_SCALE_BICUBIC); - addScaleFilter("Basic.Settings.Video.DownscaleFilter.Lanczos", OBS_SCALE_LANCZOS); - - auto connectScaleFilter = [&](QComboBox *filter, QComboBox *res) -> void { - connect(filter, &QComboBox::currentIndexChanged, this, - [this, res, filter](int) { res->setEnabled(filter->currentData() != OBS_SCALE_DISABLE); }); - }; - - connectScaleFilter(ui->advOutRescaleFilter, ui->advOutRescale); - connectScaleFilter(ui->advOutRecRescaleFilter, ui->advOutRecRescale); - - // Get Bind to IP Addresses - obs_properties_t *ppts = obs_get_output_properties("rtmp_output"); - obs_property_t *p = obs_properties_get(ppts, "bind_ip"); - - size_t count = obs_property_list_item_count(p); - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *val = obs_property_list_item_string(p, i); - - ui->bindToIP->addItem(QT_UTF8(name), val); - } - - // Add IP Family options - p = obs_properties_get(ppts, "ip_family"); - - count = obs_property_list_item_count(p); - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *val = obs_property_list_item_string(p, i); - - ui->ipFamily->addItem(QT_UTF8(name), val); - } - - obs_properties_destroy(ppts); - - ui->multitrackVideoNoticeBox->setVisible(false); - - InitStreamPage(); - InitAppearancePage(); - LoadSettings(false); - - ui->advOutTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); - ui->advOutTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); - ui->advOutTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); - ui->advOutTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); - ui->advOutTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); - ui->advOutTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); - - ui->advOutRecTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); - ui->advOutRecTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); - ui->advOutRecTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); - ui->advOutRecTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); - ui->advOutRecTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); - ui->advOutRecTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); - - ui->advOutFFTrack1->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track1")); - ui->advOutFFTrack2->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track2")); - ui->advOutFFTrack3->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track3")); - ui->advOutFFTrack4->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track4")); - ui->advOutFFTrack5->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track5")); - ui->advOutFFTrack6->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Audio.Track6")); - - ui->snappingEnabled->setAccessibleName(QTStr("Basic.Settings.General.Snapping")); - ui->systemTrayEnabled->setAccessibleName(QTStr("Basic.Settings.General.SysTray")); - ui->label_31->setAccessibleName(QTStr("Basic.Settings.Output.Adv.Recording.RecType")); - ui->streamDelayEnable->setAccessibleName(QTStr("Basic.Settings.Advanced.StreamDelay")); - ui->reconnectEnable->setAccessibleName(QTStr("Basic.Settings.Output.Reconnect")); - - // Add warning checks to advanced output recording section controls - connect(ui->advOutRecTrack1, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecTrack2, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecTrack3, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecTrack4, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecTrack5, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecTrack6, &QCheckBox::clicked, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecFormat, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - connect(ui->advOutRecEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckWarnings); - - // Check codec compatibility when format (container) changes - connect(ui->advOutRecFormat, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvOutRecCheckCodecs); - - // Set placeholder used when selection was reset due to incompatibilities - ui->advOutAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); - ui->advOutRecEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); - ui->advOutRecAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); - ui->simpleOutRecEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); - ui->simpleOutRecAEncoder->setPlaceholderText(QTStr("CodecCompat.CodecPlaceholder")); - ui->simpleOutRecFormat->setPlaceholderText(QTStr("CodecCompat.ContainerPlaceholder")); - - SimpleRecordingQualityChanged(); - AdvOutSplitFileChanged(); - AdvOutRecCheckCodecs(); - AdvOutRecCheckWarnings(); - - UpdateAutomaticReplayBufferCheckboxes(); - - App()->DisableHotkeys(); - - channelIndex = ui->channelSetup->currentIndex(); - sampleRateIndex = ui->sampleRate->currentIndex(); - llBufferingEnabled = ui->lowLatencyBuffering->isChecked(); - - QRegularExpression rx("\\d{1,5}x\\d{1,5}"); - QValidator *validator = new QRegularExpressionValidator(rx, this); - ui->baseResolution->lineEdit()->setValidator(validator); - ui->outputResolution->lineEdit()->setValidator(validator); - ui->advOutRescale->lineEdit()->setValidator(validator); - ui->advOutRecRescale->lineEdit()->setValidator(validator); - ui->advOutFFRescale->lineEdit()->setValidator(validator); - - connect(ui->useStreamKeyAdv, &QCheckBox::clicked, this, &OBSBasicSettings::UseStreamKeyAdvClicked); - - connect(ui->simpleOutStrAEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::SimpleStreamAudioEncoderChanged); - connect(ui->advOutAEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvAudioEncodersChanged); - connect(ui->advOutRecAEncoder, &QComboBox::currentIndexChanged, this, - &OBSBasicSettings::AdvAudioEncodersChanged); - - UpdateAudioWarnings(); - UpdateAdvNetworkGroup(); - - ui->audioMsg->setVisible(false); - ui->advancedMsg->setVisible(false); - ui->advancedMsg2->setVisible(false); -} - -OBSBasicSettings::~OBSBasicSettings() -{ - delete ui->filenameFormatting->completer(); - main->EnableOutputs(true); - - App()->UpdateHotkeyFocusSetting(); - - EnableThreadedMessageBoxes(false); -} - -void OBSBasicSettings::SaveCombo(QComboBox *widget, const char *section, const char *value) -{ - if (WidgetChanged(widget)) - config_set_string(main->Config(), section, value, QT_TO_UTF8(widget->currentText())); -} - -void OBSBasicSettings::SaveComboData(QComboBox *widget, const char *section, const char *value) -{ - if (WidgetChanged(widget)) { - QString str = GetComboData(widget); - config_set_string(main->Config(), section, value, QT_TO_UTF8(str)); - } -} - -void OBSBasicSettings::SaveCheckBox(QAbstractButton *widget, const char *section, const char *value, bool invert) -{ - if (WidgetChanged(widget)) { - bool checked = widget->isChecked(); - if (invert) - checked = !checked; - - config_set_bool(main->Config(), section, value, checked); - } -} - -void OBSBasicSettings::SaveEdit(QLineEdit *widget, const char *section, const char *value) -{ - if (WidgetChanged(widget)) - config_set_string(main->Config(), section, value, QT_TO_UTF8(widget->text())); -} - -void OBSBasicSettings::SaveSpinBox(QSpinBox *widget, const char *section, const char *value) -{ - if (WidgetChanged(widget)) - config_set_int(main->Config(), section, value, widget->value()); -} - -void OBSBasicSettings::SaveText(QPlainTextEdit *widget, const char *section, const char *value) -{ - if (!WidgetChanged(widget)) - return; - - auto utf8 = widget->toPlainText().toUtf8(); - - OBSDataAutoRelease safe_text = obs_data_create(); - obs_data_set_string(safe_text, "text", utf8.constData()); - - config_set_string(main->Config(), section, value, obs_data_get_json(safe_text)); -} - -std::string DeserializeConfigText(const char *value) -{ - OBSDataAutoRelease data = obs_data_create_from_json(value); - return obs_data_get_string(data, "text"); -} - -void OBSBasicSettings::SaveGroupBox(QGroupBox *widget, const char *section, const char *value) -{ - if (WidgetChanged(widget)) - config_set_bool(main->Config(), section, value, widget->isChecked()); -} - -#define CS_PARTIAL_STR QTStr("Basic.Settings.Advanced.Video.ColorRange.Partial") -#define CS_FULL_STR QTStr("Basic.Settings.Advanced.Video.ColorRange.Full") - -void OBSBasicSettings::LoadColorRanges() -{ - ui->colorRange->addItem(CS_PARTIAL_STR, "Partial"); - ui->colorRange->addItem(CS_FULL_STR, "Full"); -} - -#define CS_SRGB_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.sRGB") -#define CS_709_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.709") -#define CS_601_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.601") -#define CS_2100PQ_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.2100PQ") -#define CS_2100HLG_STR QTStr("Basic.Settings.Advanced.Video.ColorSpace.2100HLG") - -void OBSBasicSettings::LoadColorSpaces() -{ - ui->colorSpace->addItem(CS_SRGB_STR, "sRGB"); - ui->colorSpace->addItem(CS_709_STR, "709"); - ui->colorSpace->addItem(CS_601_STR, "601"); - ui->colorSpace->addItem(CS_2100PQ_STR, "2100PQ"); - ui->colorSpace->addItem(CS_2100HLG_STR, "2100HLG"); -} - -#define CF_NV12_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.NV12") -#define CF_I420_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I420") -#define CF_I444_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I444") -#define CF_P010_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P010") -#define CF_I010_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.I010") -#define CF_P216_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P216") -#define CF_P416_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.P416") -#define CF_BGRA_STR QTStr("Basic.Settings.Advanced.Video.ColorFormat.BGRA") - -void OBSBasicSettings::LoadColorFormats() -{ - ui->colorFormat->addItem(CF_NV12_STR, "NV12"); - ui->colorFormat->addItem(CF_I420_STR, "I420"); - ui->colorFormat->addItem(CF_I444_STR, "I444"); - ui->colorFormat->addItem(CF_P010_STR, "P010"); - ui->colorFormat->addItem(CF_I010_STR, "I010"); - ui->colorFormat->addItem(CF_P216_STR, "P216"); - ui->colorFormat->addItem(CF_P416_STR, "P416"); - ui->colorFormat->addItem(CF_BGRA_STR, "RGB"); // Avoid config break -} - -#define AV_FORMAT_DEFAULT_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatDefault") -#define AUDIO_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatAudio") -#define VIDEO_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatVideo") - -void OBSBasicSettings::LoadFormats() -{ -#define FORMAT_STR(str) QTStr("Basic.Settings.Output.Format." str) - ui->advOutFFFormat->blockSignals(true); - - formats = GetSupportedFormats(); - - for (auto &format : formats) { - bool audio = format.HasAudio(); - bool video = format.HasVideo(); - - if (audio || video) { - QString itemText(format.name); - if (audio ^ video) - itemText += QString(" (%1)").arg(audio ? AUDIO_STR : VIDEO_STR); - - ui->advOutFFFormat->addItem(itemText, QVariant::fromValue(format)); - } - } - - ui->advOutFFFormat->model()->sort(0); - - ui->advOutFFFormat->insertItem(0, AV_FORMAT_DEFAULT_STR); - - ui->advOutFFFormat->blockSignals(false); - - ui->simpleOutRecFormat->addItem(FORMAT_STR("FLV"), "flv"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("MOV"), "mov"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov"); - ui->simpleOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts"); - - ui->advOutRecFormat->addItem(FORMAT_STR("FLV"), "flv"); - ui->advOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv"); - ui->advOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4"); - ui->advOutRecFormat->addItem(FORMAT_STR("MOV"), "mov"); - ui->advOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4"); - ui->advOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4"); - ui->advOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov"); - ui->advOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts"); - ui->advOutRecFormat->addItem(FORMAT_STR("HLS"), "hls"); - -#undef FORMAT_STR -} - -static void AddCodec(QComboBox *combo, const FFmpegCodec &codec) -{ - QString itemText; - if (codec.long_name) - itemText = QString("%1 - %2").arg(codec.name, codec.long_name); - else - itemText = codec.name; - - combo->addItem(itemText, QVariant::fromValue(codec)); -} - -#define AV_ENCODER_DEFAULT_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.AVEncoderDefault") - -static void AddDefaultCodec(QComboBox *combo, const FFmpegFormat &format, FFmpegCodecType codecType) -{ - FFmpegCodec codec = format.GetDefaultEncoder(codecType); - - int existingIdx = FindEncoder(combo, codec.name, codec.id); - if (existingIdx >= 0) - combo->removeItem(existingIdx); - - QString itemText; - if (codec.long_name) { - itemText = QString("%1 - %2 (%3)").arg(codec.name, codec.long_name, AV_ENCODER_DEFAULT_STR); - } else { - itemText = QString("%1 (%2)").arg(codec.name, AV_ENCODER_DEFAULT_STR); - } - - combo->addItem(itemText, QVariant::fromValue(codec)); -} - -#define AV_ENCODER_DISABLE_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.AVEncoderDisable") - -void OBSBasicSettings::ReloadCodecs(const FFmpegFormat &format) -{ - ui->advOutFFAEncoder->blockSignals(true); - ui->advOutFFVEncoder->blockSignals(true); - ui->advOutFFAEncoder->clear(); - ui->advOutFFVEncoder->clear(); - - bool ignore_compatibility = ui->advOutFFIgnoreCompat->isChecked(); - vector supportedCodecs = GetFormatCodecs(format, ignore_compatibility); - - for (auto &codec : supportedCodecs) { - switch (codec.type) { - case FFmpegCodecType::AUDIO: - AddCodec(ui->advOutFFAEncoder, codec); - break; - case FFmpegCodecType::VIDEO: - AddCodec(ui->advOutFFVEncoder, codec); - break; - default: - break; - } - } - - if (format.HasAudio()) - AddDefaultCodec(ui->advOutFFAEncoder, format, FFmpegCodecType::AUDIO); - if (format.HasVideo()) - AddDefaultCodec(ui->advOutFFVEncoder, format, FFmpegCodecType::VIDEO); - - ui->advOutFFAEncoder->model()->sort(0); - ui->advOutFFVEncoder->model()->sort(0); - - QVariant disable = QVariant::fromValue(FFmpegCodec()); - - ui->advOutFFAEncoder->insertItem(0, AV_ENCODER_DISABLE_STR, disable); - ui->advOutFFVEncoder->insertItem(0, AV_ENCODER_DISABLE_STR, disable); - - ui->advOutFFAEncoder->blockSignals(false); - ui->advOutFFVEncoder->blockSignals(false); -} - -void OBSBasicSettings::LoadLanguageList() -{ - const char *currentLang = App()->GetLocale(); - - ui->language->clear(); - - for (const auto &locale : GetLocaleNames()) { - int idx = ui->language->count(); - - ui->language->addItem(QT_UTF8(locale.second.c_str()), QT_UTF8(locale.first.c_str())); - - if (locale.first == currentLang) - ui->language->setCurrentIndex(idx); - } - - ui->language->model()->sort(0); -} - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -void TranslateBranchInfo(const QString &name, QString &displayName, QString &description) -{ - QString translatedName = QTStr("Basic.Settings.General.ChannelName." + name.toUtf8()); - QString translatedDesc = QTStr("Basic.Settings.General.ChannelDescription." + name.toUtf8()); - - if (!translatedName.startsWith("Basic.Settings.")) - displayName = translatedName; - if (!translatedDesc.startsWith("Basic.Settings.")) - description = translatedDesc; -} -#endif - -void OBSBasicSettings::LoadBranchesList() -{ -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - bool configBranchRemoved = true; - QString configBranch = config_get_string(App()->GetAppConfig(), "General", "UpdateBranch"); - - for (const UpdateBranch &branch : App()->GetBranches()) { - if (branch.name == configBranch) - configBranchRemoved = false; - if (!branch.is_visible && branch.name != configBranch) - continue; - - QString displayName = branch.display_name; - QString description = branch.description; - - TranslateBranchInfo(branch.name, displayName, description); - QString itemDesc = displayName + " - " + description; - - if (!branch.is_enabled) { - itemDesc.prepend(" "); - itemDesc.prepend(QTStr("Basic.Settings.General.UpdateChannelDisabled")); - } else if (branch.name == "stable") { - itemDesc.append(" "); - itemDesc.append(QTStr("Basic.Settings.General.UpdateChannelDefault")); - } - - ui->updateChannelBox->addItem(itemDesc, branch.name); - - // Disable item if branch is disabled - if (!branch.is_enabled) { - QStandardItemModel *model = dynamic_cast(ui->updateChannelBox->model()); - QStandardItem *item = model->item(ui->updateChannelBox->count() - 1); - item->setFlags(Qt::NoItemFlags); - } - } - - // Fall back to default if not yet set or user-selected branch has been removed - if (configBranch.isEmpty() || configBranchRemoved) - configBranch = "stable"; - - int idx = ui->updateChannelBox->findData(configBranch); - ui->updateChannelBox->setCurrentIndex(idx); -#endif -} - -void OBSBasicSettings::LoadGeneralSettings() -{ - loading = true; - - LoadLanguageList(); - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - bool enableAutoUpdates = config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates"); - ui->enableAutoUpdates->setChecked(enableAutoUpdates); - - LoadBranchesList(); -#endif - bool openStatsOnStartup = config_get_bool(main->Config(), "General", "OpenStatsOnStartup"); - ui->openStatsOnStartup->setChecked(openStatsOnStartup); - -#if defined(_WIN32) - if (ui->hideOBSFromCapture) { - bool hideWindowFromCapture = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - ui->hideOBSFromCapture->setChecked(hideWindowFromCapture); - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - connect(ui->hideOBSFromCapture, &QCheckBox::checkStateChanged, this, - &OBSBasicSettings::HideOBSWindowWarning); -#else - connect(ui->hideOBSFromCapture, &QCheckBox::stateChanged, this, - &OBSBasicSettings::HideOBSWindowWarning); -#endif - } -#endif - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - ui->recordWhenStreaming->setChecked(recordWhenStreaming); - - bool keepRecordStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - ui->keepRecordStreamStops->setChecked(keepRecordStreamStops); - - bool replayWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - ui->replayWhileStreaming->setChecked(replayWhileStreaming); - - bool keepReplayStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - ui->keepReplayStreamStops->setChecked(keepReplayStreamStops); - - bool systemTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - ui->systemTrayEnabled->setChecked(systemTrayEnabled); - - bool systemTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - ui->systemTrayWhenStarted->setChecked(systemTrayWhenStarted); - - bool systemTrayAlways = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); - ui->systemTrayAlways->setChecked(systemTrayAlways); - - bool saveProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - ui->saveProjectors->setChecked(saveProjectors); - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - ui->closeProjectors->setChecked(closeProjectors); - - bool snappingEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SnappingEnabled"); - ui->snappingEnabled->setChecked(snappingEnabled); - - bool screenSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ScreenSnapping"); - ui->screenSnapping->setChecked(screenSnapping); - - bool centerSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CenterSnapping"); - ui->centerSnapping->setChecked(centerSnapping); - - bool sourceSnapping = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SourceSnapping"); - ui->sourceSnapping->setChecked(sourceSnapping); - - double snapDistance = config_get_double(App()->GetUserConfig(), "BasicWindow", "SnapDistance"); - ui->snapDistance->setValue(snapDistance); - - bool warnBeforeStreamStart = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - ui->warnBeforeStreamStart->setChecked(warnBeforeStreamStart); - - bool spacingHelpersEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); - ui->previewSpacingHelpers->setChecked(spacingHelpersEnabled); - - bool warnBeforeStreamStop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - ui->warnBeforeStreamStop->setChecked(warnBeforeStreamStop); - - bool warnBeforeRecordStop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - ui->warnBeforeRecordStop->setChecked(warnBeforeRecordStop); - - bool hideProjectorCursor = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideProjectorCursor"); - ui->hideProjectorCursor->setChecked(hideProjectorCursor); - - bool projectorAlwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ProjectorAlwaysOnTop"); - ui->projectorAlwaysOnTop->setChecked(projectorAlwaysOnTop); - - bool overflowHide = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - ui->overflowHide->setChecked(overflowHide); - - bool overflowAlwaysVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - ui->overflowAlwaysVisible->setChecked(overflowAlwaysVisible); - - bool overflowSelectionHide = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - ui->overflowSelectionHide->setChecked(overflowSelectionHide); - - bool safeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); - ui->previewSafeAreas->setChecked(safeAreas); - - bool automaticSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); - ui->automaticSearch->setChecked(automaticSearch); - - bool doubleClickSwitch = config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - ui->doubleClickSwitch->setChecked(doubleClickSwitch); - - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - ui->studioPortraitLayout->setChecked(studioPortraitLayout); - - bool prevProgLabels = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels"); - ui->prevProgLabelToggle->setChecked(prevProgLabels); - - bool multiviewMouseSwitch = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewMouseSwitch"); - ui->multiviewMouseSwitch->setChecked(multiviewMouseSwitch); - - bool multiviewDrawNames = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawNames"); - ui->multiviewDrawNames->setChecked(multiviewDrawNames); - - bool multiviewDrawAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawAreas"); - ui->multiviewDrawAreas->setChecked(multiviewDrawAreas); - - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Top"), - static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Bottom"), - static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Vertical.Left"), - static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Vertical.Right"), - static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.18Scene.Top"), - static_cast(MultiviewLayout::HORIZONTAL_TOP_18_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top"), - static_cast(MultiviewLayout::HORIZONTAL_TOP_24_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.4Scene"), - static_cast(MultiviewLayout::SCENES_ONLY_4_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.9Scene"), - static_cast(MultiviewLayout::SCENES_ONLY_9_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.16Scene"), - static_cast(MultiviewLayout::SCENES_ONLY_16_SCENES)); - ui->multiviewLayout->addItem(QTStr("Basic.Settings.General.MultiviewLayout.25Scene"), - static_cast(MultiviewLayout::SCENES_ONLY_25_SCENES)); - - ui->multiviewLayout->setCurrentIndex(ui->multiviewLayout->findData( - QVariant::fromValue(config_get_int(App()->GetUserConfig(), "BasicWindow", "MultiviewLayout")))); - - prevLangIndex = ui->language->currentIndex(); - - if (obs_video_active()) - ui->language->setEnabled(false); - - loading = false; -} - -void OBSBasicSettings::LoadRendererList() -{ -#ifdef _WIN32 - const char *renderer = config_get_string(App()->GetAppConfig(), "Video", "Renderer"); - - ui->renderer->addItem(QT_UTF8("Direct3D 11")); - if (opt_allow_opengl || strcmp(renderer, "OpenGL") == 0) - ui->renderer->addItem(QT_UTF8("OpenGL")); - - int idx = ui->renderer->findText(QT_UTF8(renderer)); - if (idx == -1) - idx = 0; - - // the video adapter selection is not currently implemented, hide for now - // to avoid user confusion. was previously protected by - // if (strcmp(renderer, "OpenGL") == 0) - delete ui->adapter; - delete ui->adapterLabel; - ui->adapter = nullptr; - ui->adapterLabel = nullptr; - - ui->renderer->setCurrentIndex(idx); -#endif -} - -static string ResString(uint32_t cx, uint32_t cy) -{ - stringstream res; - res << cx << "x" << cy; - return res.str(); -} - -/* some nice default output resolution vals */ -static const double vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0}; - -static const size_t numVals = sizeof(vals) / sizeof(double); - -void OBSBasicSettings::ResetDownscales(uint32_t cx, uint32_t cy, bool ignoreAllSignals) -{ - QString advRescale; - QString advRecRescale; - QString advFFRescale; - QString oldOutputRes; - string bestScale; - int bestPixelDiff = 0x7FFFFFFF; - uint32_t out_cx = outputCX; - uint32_t out_cy = outputCY; - - advRescale = ui->advOutRescale->lineEdit()->text(); - advRecRescale = ui->advOutRecRescale->lineEdit()->text(); - advFFRescale = ui->advOutFFRescale->lineEdit()->text(); - - bool lockedOutputRes = !ui->outputResolution->isEditable(); - - if (!lockedOutputRes) { - ui->outputResolution->blockSignals(true); - ui->outputResolution->clear(); - } - if (ignoreAllSignals) { - ui->advOutRescale->blockSignals(true); - ui->advOutRecRescale->blockSignals(true); - ui->advOutFFRescale->blockSignals(true); - } - ui->advOutRescale->clear(); - ui->advOutRecRescale->clear(); - ui->advOutFFRescale->clear(); - - if (!out_cx || !out_cy) { - out_cx = cx; - out_cy = cy; - oldOutputRes = ui->baseResolution->lineEdit()->text(); - } else { - oldOutputRes = QString::number(out_cx) + "x" + QString::number(out_cy); - } - - for (size_t idx = 0; idx < numVals; idx++) { - uint32_t downscaleCX = uint32_t(double(cx) / vals[idx]); - uint32_t downscaleCY = uint32_t(double(cy) / vals[idx]); - uint32_t outDownscaleCX = uint32_t(double(out_cx) / vals[idx]); - uint32_t outDownscaleCY = uint32_t(double(out_cy) / vals[idx]); - - downscaleCX &= 0xFFFFFFFC; - downscaleCY &= 0xFFFFFFFE; - outDownscaleCX &= 0xFFFFFFFE; - outDownscaleCY &= 0xFFFFFFFE; - - string res = ResString(downscaleCX, downscaleCY); - string outRes = ResString(outDownscaleCX, outDownscaleCY); - if (!lockedOutputRes) - ui->outputResolution->addItem(res.c_str()); - ui->advOutRescale->addItem(outRes.c_str()); - ui->advOutRecRescale->addItem(outRes.c_str()); - ui->advOutFFRescale->addItem(outRes.c_str()); - - /* always try to find the closest output resolution to the - * previously set output resolution */ - int newPixelCount = int(downscaleCX * downscaleCY); - int oldPixelCount = int(out_cx * out_cy); - int diff = abs(newPixelCount - oldPixelCount); - - if (diff < bestPixelDiff) { - bestScale = res; - bestPixelDiff = diff; - } - } - - string res = ResString(cx, cy); - - if (!lockedOutputRes) { - float baseAspect = float(cx) / float(cy); - float outputAspect = float(out_cx) / float(out_cy); - bool closeAspect = close_float(baseAspect, outputAspect, 0.01f); - - if (closeAspect) { - ui->outputResolution->lineEdit()->setText(oldOutputRes); - on_outputResolution_editTextChanged(oldOutputRes); - } else { - ui->outputResolution->lineEdit()->setText(bestScale.c_str()); - on_outputResolution_editTextChanged(bestScale.c_str()); - } - - ui->outputResolution->blockSignals(false); - - if (!closeAspect) { - ui->outputResolution->setProperty("changed", QVariant(true)); - videoChanged = true; - } - } - - if (advRescale.isEmpty()) - advRescale = res.c_str(); - if (advRecRescale.isEmpty()) - advRecRescale = res.c_str(); - if (advFFRescale.isEmpty()) - advFFRescale = res.c_str(); - - ui->advOutRescale->lineEdit()->setText(advRescale); - ui->advOutRecRescale->lineEdit()->setText(advRecRescale); - ui->advOutFFRescale->lineEdit()->setText(advFFRescale); - - if (ignoreAllSignals) { - ui->advOutRescale->blockSignals(false); - ui->advOutRecRescale->blockSignals(false); - ui->advOutFFRescale->blockSignals(false); - } -} - -void OBSBasicSettings::LoadDownscaleFilters() -{ - QString downscaleFilter = ui->downscaleFilter->currentData().toString(); - if (downscaleFilter.isEmpty()) - downscaleFilter = config_get_string(main->Config(), "Video", "ScaleType"); - - ui->downscaleFilter->clear(); - if (ui->baseResolution->currentText() == ui->outputResolution->currentText()) { - ui->downscaleFilter->setEnabled(false); - ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Unavailable"), - downscaleFilter); - } else { - ui->downscaleFilter->setEnabled(true); - ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Bilinear"), - QT_UTF8("bilinear")); - ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Area"), QT_UTF8("area")); - ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Bicubic"), QT_UTF8("bicubic")); - ui->downscaleFilter->addItem(QTStr("Basic.Settings.Video.DownscaleFilter.Lanczos"), QT_UTF8("lanczos")); - - if (downscaleFilter == "bilinear") - ui->downscaleFilter->setCurrentIndex(0); - else if (downscaleFilter == "lanczos") - ui->downscaleFilter->setCurrentIndex(3); - else if (downscaleFilter == "area") - ui->downscaleFilter->setCurrentIndex(1); - else - ui->downscaleFilter->setCurrentIndex(2); - } -} - -void OBSBasicSettings::LoadResolutionLists() -{ - uint32_t cx = config_get_uint(main->Config(), "Video", "BaseCX"); - uint32_t cy = config_get_uint(main->Config(), "Video", "BaseCY"); - uint32_t out_cx = config_get_uint(main->Config(), "Video", "OutputCX"); - uint32_t out_cy = config_get_uint(main->Config(), "Video", "OutputCY"); - - ui->baseResolution->clear(); - - auto addRes = [this](int cx, int cy) { - QString res = ResString(cx, cy).c_str(); - if (ui->baseResolution->findText(res) == -1) - ui->baseResolution->addItem(res); - }; - - for (QScreen *screen : QGuiApplication::screens()) { - QSize as = screen->size(); - uint32_t as_width = as.width(); - uint32_t as_height = as.height(); - - // Calculate physical screen resolution based on the virtual screen resolution - // They might differ if scaling is enabled, e.g. for HiDPI screens - as_width = round(as_width * screen->devicePixelRatio()); - as_height = round(as_height * screen->devicePixelRatio()); - - addRes(as_width, as_height); - } - - addRes(1920, 1080); - addRes(1280, 720); - - string outputResString = ResString(out_cx, out_cy); - - ui->baseResolution->lineEdit()->setText(ResString(cx, cy).c_str()); - - RecalcOutputResPixels(outputResString.c_str()); - ResetDownscales(cx, cy); - - ui->outputResolution->lineEdit()->setText(outputResString.c_str()); - - std::tuple aspect = aspect_ratio(cx, cy); - - ui->baseAspect->setText( - QTStr("AspectRatio").arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); -} - -static inline void LoadFPSCommon(OBSBasic *main, Ui::OBSBasicSettings *ui) -{ - const char *val = config_get_string(main->Config(), "Video", "FPSCommon"); - - int idx = ui->fpsCommon->findText(val); - if (idx == -1) - idx = 4; - ui->fpsCommon->setCurrentIndex(idx); -} - -static inline void LoadFPSInteger(OBSBasic *main, Ui::OBSBasicSettings *ui) -{ - uint32_t val = config_get_uint(main->Config(), "Video", "FPSInt"); - ui->fpsInteger->setValue(val); -} - -static inline void LoadFPSFraction(OBSBasic *main, Ui::OBSBasicSettings *ui) -{ - uint32_t num = config_get_uint(main->Config(), "Video", "FPSNum"); - uint32_t den = config_get_uint(main->Config(), "Video", "FPSDen"); - - ui->fpsNumerator->setValue(num); - ui->fpsDenominator->setValue(den); -} - -void OBSBasicSettings::LoadFPSData() -{ - LoadFPSCommon(main, ui.get()); - LoadFPSInteger(main, ui.get()); - LoadFPSFraction(main, ui.get()); - - uint32_t fpsType = config_get_uint(main->Config(), "Video", "FPSType"); - if (fpsType > 2) - fpsType = 0; - - ui->fpsType->setCurrentIndex(fpsType); - ui->fpsTypes->setCurrentIndex(fpsType); -} - -void OBSBasicSettings::LoadVideoSettings() -{ - loading = true; - - if (obs_video_active()) { - ui->videoPage->setEnabled(false); - ui->videoMsg->setText(QTStr("Basic.Settings.Video.CurrentlyActive")); - } - - LoadResolutionLists(); - LoadFPSData(); - LoadDownscaleFilters(); - - loading = false; -} - -static inline bool IsSurround(const char *speakers) -{ - static const char *surroundLayouts[] = {"2.1", "4.0", "4.1", "5.1", "7.1", nullptr}; - - if (!speakers || !*speakers) - return false; - - const char **curLayout = surroundLayouts; - for (; *curLayout; ++curLayout) { - if (strcmp(*curLayout, speakers) == 0) { - return true; - } - } - - return false; -} - -void OBSBasicSettings::LoadSimpleOutputSettings() -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - const char *streamEnc = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *streamAudioEnc = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int audioBitrate = config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - const char *preset = config_get_string(main->Config(), "SimpleOutput", "Preset"); - const char *qsvPreset = config_get_string(main->Config(), "SimpleOutput", "QSVPreset"); - const char *nvPreset = config_get_string(main->Config(), "SimpleOutput", "NVENCPreset2"); - const char *amdPreset = config_get_string(main->Config(), "SimpleOutput", "AMDPreset"); - const char *amdAV1Preset = config_get_string(main->Config(), "SimpleOutput", "AMDAV1Preset"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *recQual = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *recEnc = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *recAudioEnc = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - const char *muxCustom = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool replayBuf = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - ui->simpleOutRecTrack1->setChecked(tracks & (1 << 0)); - ui->simpleOutRecTrack2->setChecked(tracks & (1 << 1)); - ui->simpleOutRecTrack3->setChecked(tracks & (1 << 2)); - ui->simpleOutRecTrack4->setChecked(tracks & (1 << 3)); - ui->simpleOutRecTrack5->setChecked(tracks & (1 << 4)); - ui->simpleOutRecTrack6->setChecked(tracks & (1 << 5)); - - curPreset = preset; - curQSVPreset = qsvPreset; - curNVENCPreset = nvPreset; - curAMDPreset = amdPreset; - curAMDAV1Preset = amdAV1Preset; - - bool isOpus = strcmp(streamAudioEnc, "opus") == 0; - audioBitrate = isOpus ? FindClosestAvailableSimpleOpusBitrate(audioBitrate) - : FindClosestAvailableSimpleAACBitrate(audioBitrate); - - ui->simpleOutputPath->setText(path); - ui->simpleNoSpace->setChecked(noSpace); - ui->simpleOutputVBitrate->setValue(videoBitrate); - - int idx = ui->simpleOutRecFormat->findData(format); - ui->simpleOutRecFormat->setCurrentIndex(idx); - - PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); - - const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); - - // restrict list of bitrates when multichannel is OFF - if (!IsSurround(speakers)) - RestrictResetBitrates({ui->simpleOutputABitrate}, 320); - - SetComboByName(ui->simpleOutputABitrate, std::to_string(audioBitrate).c_str()); - - ui->simpleOutAdvanced->setChecked(advanced); - ui->simpleOutCustom->setText(custom); - - idx = ui->simpleOutRecQuality->findData(QString(recQual)); - if (idx == -1) - idx = 0; - ui->simpleOutRecQuality->setCurrentIndex(idx); - - idx = ui->simpleOutStrEncoder->findData(QString(streamEnc)); - if (idx == -1) - idx = 0; - ui->simpleOutStrEncoder->setCurrentIndex(idx); - - idx = ui->simpleOutStrAEncoder->findData(QString(streamAudioEnc)); - if (idx == -1) - idx = 0; - ui->simpleOutStrAEncoder->setCurrentIndex(idx); - - idx = ui->simpleOutRecEncoder->findData(QString(recEnc)); - ui->simpleOutRecEncoder->setCurrentIndex(idx); - - idx = ui->simpleOutRecAEncoder->findData(QString(recAudioEnc)); - ui->simpleOutRecAEncoder->setCurrentIndex(idx); - - ui->simpleOutMuxCustom->setText(muxCustom); - - ui->simpleReplayBuf->setChecked(replayBuf); - ui->simpleRBSecMax->setValue(rbTime); - ui->simpleRBMegsMax->setValue(rbSize); - - SimpleStreamingEncoderChanged(); -} - -static inline QString makeFormatToolTip() -{ - static const char *format_list[][2] = { - {"CCYY", "FilenameFormatting.TT.CCYY"}, {"YY", "FilenameFormatting.TT.YY"}, - {"MM", "FilenameFormatting.TT.MM"}, {"DD", "FilenameFormatting.TT.DD"}, - {"hh", "FilenameFormatting.TT.hh"}, {"mm", "FilenameFormatting.TT.mm"}, - {"ss", "FilenameFormatting.TT.ss"}, {"%", "FilenameFormatting.TT.Percent"}, - {"a", "FilenameFormatting.TT.a"}, {"A", "FilenameFormatting.TT.A"}, - {"b", "FilenameFormatting.TT.b"}, {"B", "FilenameFormatting.TT.B"}, - {"d", "FilenameFormatting.TT.d"}, {"H", "FilenameFormatting.TT.H"}, - {"I", "FilenameFormatting.TT.I"}, {"m", "FilenameFormatting.TT.m"}, - {"M", "FilenameFormatting.TT.M"}, {"p", "FilenameFormatting.TT.p"}, - {"s", "FilenameFormatting.TT.s"}, {"S", "FilenameFormatting.TT.S"}, - {"y", "FilenameFormatting.TT.y"}, {"Y", "FilenameFormatting.TT.Y"}, - {"z", "FilenameFormatting.TT.z"}, {"Z", "FilenameFormatting.TT.Z"}, - {"FPS", "FilenameFormatting.TT.FPS"}, {"CRES", "FilenameFormatting.TT.CRES"}, - {"ORES", "FilenameFormatting.TT.ORES"}, {"VF", "FilenameFormatting.TT.VF"}, - }; - - QString html = ""; - - for (auto f : format_list) { - html += ""; - } - - html += "
%"; - html += f[0]; - html += ""; - html += QTStr(f[1]); - html += "
"; - return html; -} - -void OBSBasicSettings::LoadAdvOutputStreamingSettings() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int trackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int audioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - ui->advOutRescale->setEnabled(rescaleFilter != OBS_SCALE_DISABLE); - ui->advOutRescale->setCurrentText(rescaleRes); - - int idx = ui->advOutRescaleFilter->findData(rescaleFilter); - if (idx != -1) - ui->advOutRescaleFilter->setCurrentIndex(idx); - - QStringList specList = QTStr("FilenameFormatting.completer").split(QRegularExpression("\n")); - QCompleter *specCompleter = new QCompleter(specList); - specCompleter->setCaseSensitivity(Qt::CaseSensitive); - specCompleter->setFilterMode(Qt::MatchContains); - ui->filenameFormatting->setCompleter(specCompleter); - ui->filenameFormatting->setToolTip(makeFormatToolTip()); - - switch (trackIndex) { - case 1: - ui->advOutTrack1->setChecked(true); - break; - case 2: - ui->advOutTrack2->setChecked(true); - break; - case 3: - ui->advOutTrack3->setChecked(true); - break; - case 4: - ui->advOutTrack4->setChecked(true); - break; - case 5: - ui->advOutTrack5->setChecked(true); - break; - case 6: - ui->advOutTrack6->setChecked(true); - break; - } - ui->advOutMultiTrack1->setChecked(audioMixes & (1 << 0)); - ui->advOutMultiTrack2->setChecked(audioMixes & (1 << 1)); - ui->advOutMultiTrack3->setChecked(audioMixes & (1 << 2)); - ui->advOutMultiTrack4->setChecked(audioMixes & (1 << 3)); - ui->advOutMultiTrack5->setChecked(audioMixes & (1 << 4)); - ui->advOutMultiTrack6->setChecked(audioMixes & (1 << 5)); - - obs_service_t *service_obj = main->GetService(); - const char *protocol = nullptr; - protocol = obs_service_get_protocol(service_obj); - SwapMultiTrack(protocol); -} - -OBSPropertiesView *OBSBasicSettings::CreateEncoderPropertyView(const char *encoder, const char *path, bool changed) -{ - OBSDataAutoRelease settings = obs_encoder_defaults(encoder); - OBSPropertiesView *view; - - if (path) { - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(path); - - if (!jsonFilePath.empty()) { - obs_data_t *data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - obs_data_apply(settings, data); - obs_data_release(data); - } - } - - view = new OBSPropertiesView(settings.Get(), encoder, (PropertiesReloadCallback)obs_get_encoder_properties, - 170); - view->setFrameShape(QFrame::NoFrame); - view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - view->setProperty("changed", QVariant(changed)); - view->setScrolling(false); - QObject::connect(view, &OBSPropertiesView::Changed, this, &OBSBasicSettings::OutputsChanged); - - return view; -} - -void OBSBasicSettings::LoadAdvOutputStreamingEncoderProperties() -{ - const char *type = config_get_string(main->Config(), "AdvOut", "Encoder"); - - delete streamEncoderProps; - streamEncoderProps = CreateEncoderPropertyView(type, "streamEncoder.json"); - streamEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - ui->advOutEncoderLayout->addWidget(streamEncoderProps); - - connect(streamEncoderProps, &OBSPropertiesView::Changed, this, &OBSBasicSettings::UpdateStreamDelayEstimate); - connect(streamEncoderProps, &OBSPropertiesView::Changed, this, &OBSBasicSettings::AdvReplayBufferChanged); - - curAdvStreamEncoder = type; - - if (!SetComboByValue(ui->advOutEncoder, type)) { - uint32_t caps = obs_get_encoder_caps(type); - if ((caps & ENCODER_HIDE_FLAGS) != 0) { - QString encName = QT_UTF8(obs_encoder_get_display_name(type)); - if (caps & OBS_ENCODER_CAP_DEPRECATED) - encName += " (" + QTStr("Deprecated") + ")"; - - ui->advOutEncoder->insertItem(0, encName, QT_UTF8(type)); - SetComboByValue(ui->advOutEncoder, type); - } - } - - UpdateStreamDelayEstimate(); -} - -void OBSBasicSettings::LoadAdvOutputRecordingSettings() -{ - const char *type = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *format = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - bool noSpace = config_get_bool(main->Config(), "AdvOut", "RecFileNameWithoutSpace"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - int tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - int flvTrack = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - bool splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - const char *splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - int splitFileTime = config_get_int(main->Config(), "AdvOut", "RecSplitFileTime"); - int splitFileSize = config_get_int(main->Config(), "AdvOut", "RecSplitFileSize"); - - int typeIndex = (astrcmpi(type, "FFmpeg") == 0) ? 1 : 0; - ui->advOutRecType->setCurrentIndex(typeIndex); - ui->advOutRecPath->setText(path); - ui->advOutNoSpace->setChecked(noSpace); - ui->advOutRecRescale->setCurrentText(rescaleRes); - int idx = ui->advOutRecRescaleFilter->findData(rescaleFilter); - if (idx != -1) - ui->advOutRecRescaleFilter->setCurrentIndex(idx); - ui->advOutMuxCustom->setText(muxCustom); - - idx = ui->advOutRecFormat->findData(format); - ui->advOutRecFormat->setCurrentIndex(idx); - - ui->advOutRecTrack1->setChecked(tracks & (1 << 0)); - ui->advOutRecTrack2->setChecked(tracks & (1 << 1)); - ui->advOutRecTrack3->setChecked(tracks & (1 << 2)); - ui->advOutRecTrack4->setChecked(tracks & (1 << 3)); - ui->advOutRecTrack5->setChecked(tracks & (1 << 4)); - ui->advOutRecTrack6->setChecked(tracks & (1 << 5)); - - if (astrcmpi(splitFileType, "Size") == 0) - idx = 1; - else if (astrcmpi(splitFileType, "Manual") == 0) - idx = 2; - else - idx = 0; - ui->advOutSplitFile->setChecked(splitFile); - ui->advOutSplitFileType->setCurrentIndex(idx); - ui->advOutSplitFileTime->setValue(splitFileTime); - ui->advOutSplitFileSize->setValue(splitFileSize); - - switch (flvTrack) { - case 1: - ui->flvTrack1->setChecked(true); - break; - case 2: - ui->flvTrack2->setChecked(true); - break; - case 3: - ui->flvTrack3->setChecked(true); - break; - case 4: - ui->flvTrack4->setChecked(true); - break; - case 5: - ui->flvTrack5->setChecked(true); - break; - case 6: - ui->flvTrack6->setChecked(true); - break; - default: - ui->flvTrack1->setChecked(true); - break; - } -} - -void OBSBasicSettings::LoadAdvOutputRecordingEncoderProperties() -{ - const char *type = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - - delete recordEncoderProps; - recordEncoderProps = nullptr; - - if (astrcmpi(type, "none") != 0) { - recordEncoderProps = CreateEncoderPropertyView(type, "recordEncoder.json"); - recordEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - ui->advOutRecEncoderProps->layout()->addWidget(recordEncoderProps); - connect(recordEncoderProps, &OBSPropertiesView::Changed, this, - &OBSBasicSettings::AdvReplayBufferChanged); - } - - curAdvRecordEncoder = type; - - if (!SetComboByValue(ui->advOutRecEncoder, type)) { - uint32_t caps = obs_get_encoder_caps(type); - if ((caps & ENCODER_HIDE_FLAGS) != 0) { - QString encName = QT_UTF8(obs_encoder_get_display_name(type)); - if (caps & OBS_ENCODER_CAP_DEPRECATED) - encName += " (" + QTStr("Deprecated") + ")"; - - ui->advOutRecEncoder->insertItem(1, encName, QT_UTF8(type)); - SetComboByValue(ui->advOutRecEncoder, type); - } else { - ui->advOutRecEncoder->setCurrentIndex(-1); - } - } -} - -static void SelectFormat(QComboBox *combo, const char *name, const char *mimeType) -{ - FFmpegFormat format{name, mimeType}; - - for (int i = 0; i < combo->count(); i++) { - QVariant v = combo->itemData(i); - if (!v.isNull()) { - if (format == v.value()) { - combo->setCurrentIndex(i); - return; - } - } - } - - combo->setCurrentIndex(0); -} - -static void SelectEncoder(QComboBox *combo, const char *name, int id) -{ - int idx = FindEncoder(combo, name, id); - if (idx >= 0) - combo->setCurrentIndex(idx); -} - -void OBSBasicSettings::LoadAdvOutputFFmpegSettings() -{ - bool saveFile = config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - const char *path = config_get_string(main->Config(), "AdvOut", "FFFilePath"); - bool noSpace = config_get_bool(main->Config(), "AdvOut", "FFFileNameWithoutSpace"); - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - const char *format = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - int videoBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - bool codecCompat = config_get_bool(main->Config(), "AdvOut", "FFIgnoreCompat"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int audioBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int audioMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - ui->advOutFFType->setCurrentIndex(saveFile ? 0 : 1); - ui->advOutFFRecPath->setText(QT_UTF8(path)); - ui->advOutFFNoSpace->setChecked(noSpace); - ui->advOutFFURL->setText(QT_UTF8(url)); - SelectFormat(ui->advOutFFFormat, format, mimeType); - ui->advOutFFMCfg->setText(muxCustom); - ui->advOutFFVBitrate->setValue(videoBitrate); - ui->advOutFFVGOPSize->setValue(gopSize); - ui->advOutFFUseRescale->setChecked(rescale); - ui->advOutFFIgnoreCompat->setChecked(codecCompat); - ui->advOutFFRescale->setEnabled(rescale); - ui->advOutFFRescale->setCurrentText(rescaleRes); - SelectEncoder(ui->advOutFFVEncoder, vEncoder, vEncoderId); - ui->advOutFFVCfg->setText(vEncCustom); - ui->advOutFFABitrate->setValue(audioBitrate); - SelectEncoder(ui->advOutFFAEncoder, aEncoder, aEncoderId); - ui->advOutFFACfg->setText(aEncCustom); - - ui->advOutFFTrack1->setChecked(audioMixes & (1 << 0)); - ui->advOutFFTrack2->setChecked(audioMixes & (1 << 1)); - ui->advOutFFTrack3->setChecked(audioMixes & (1 << 2)); - ui->advOutFFTrack4->setChecked(audioMixes & (1 << 3)); - ui->advOutFFTrack5->setChecked(audioMixes & (1 << 4)); - ui->advOutFFTrack6->setChecked(audioMixes & (1 << 5)); -} - -void OBSBasicSettings::LoadAdvOutputAudioSettings() -{ - int track1Bitrate = config_get_uint(main->Config(), "AdvOut", "Track1Bitrate"); - int track2Bitrate = config_get_uint(main->Config(), "AdvOut", "Track2Bitrate"); - int track3Bitrate = config_get_uint(main->Config(), "AdvOut", "Track3Bitrate"); - int track4Bitrate = config_get_uint(main->Config(), "AdvOut", "Track4Bitrate"); - int track5Bitrate = config_get_uint(main->Config(), "AdvOut", "Track5Bitrate"); - int track6Bitrate = config_get_uint(main->Config(), "AdvOut", "Track6Bitrate"); - const char *name1 = config_get_string(main->Config(), "AdvOut", "Track1Name"); - const char *name2 = config_get_string(main->Config(), "AdvOut", "Track2Name"); - const char *name3 = config_get_string(main->Config(), "AdvOut", "Track3Name"); - const char *name4 = config_get_string(main->Config(), "AdvOut", "Track4Name"); - const char *name5 = config_get_string(main->Config(), "AdvOut", "Track5Name"); - const char *name6 = config_get_string(main->Config(), "AdvOut", "Track6Name"); - - const char *encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *rec_encoder_id = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, - encoder_id, strcmp(rec_encoder_id, "none") != 0 ? rec_encoder_id : encoder_id); - - track1Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack1Bitrate, track1Bitrate); - track2Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack2Bitrate, track2Bitrate); - track3Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack3Bitrate, track3Bitrate); - track4Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack4Bitrate, track4Bitrate); - track5Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack5Bitrate, track5Bitrate); - track6Bitrate = FindClosestAvailableAudioBitrate(ui->advOutTrack6Bitrate, track6Bitrate); - - // restrict list of bitrates when multichannel is OFF - const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); - - // restrict list of bitrates when multichannel is OFF - if (!IsSurround(speakers)) { - RestrictResetBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, - 320); - } - - SetComboByName(ui->advOutTrack1Bitrate, std::to_string(track1Bitrate).c_str()); - SetComboByName(ui->advOutTrack2Bitrate, std::to_string(track2Bitrate).c_str()); - SetComboByName(ui->advOutTrack3Bitrate, std::to_string(track3Bitrate).c_str()); - SetComboByName(ui->advOutTrack4Bitrate, std::to_string(track4Bitrate).c_str()); - SetComboByName(ui->advOutTrack5Bitrate, std::to_string(track5Bitrate).c_str()); - SetComboByName(ui->advOutTrack6Bitrate, std::to_string(track6Bitrate).c_str()); - - ui->advOutTrack1Name->setText(name1); - ui->advOutTrack2Name->setText(name2); - ui->advOutTrack3Name->setText(name3); - ui->advOutTrack4Name->setText(name4); - ui->advOutTrack5Name->setText(name5); - ui->advOutTrack6Name->setText(name6); -} - -void OBSBasicSettings::LoadOutputSettings() -{ - loading = true; - - ResetEncoders(); - - const char *mode = config_get_string(main->Config(), "Output", "Mode"); - - int modeIdx = astrcmpi(mode, "Advanced") == 0 ? 1 : 0; - ui->outputMode->setCurrentIndex(modeIdx); - - LoadSimpleOutputSettings(); - LoadAdvOutputStreamingSettings(); - LoadAdvOutputStreamingEncoderProperties(); - - const char *type = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - if (!SetComboByValue(ui->advOutAEncoder, type)) - ui->advOutAEncoder->setCurrentIndex(-1); - - LoadAdvOutputRecordingSettings(); - LoadAdvOutputRecordingEncoderProperties(); - type = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - if (!SetComboByValue(ui->advOutRecAEncoder, type)) - ui->advOutRecAEncoder->setCurrentIndex(-1); - LoadAdvOutputFFmpegSettings(); - LoadAdvOutputAudioSettings(); - - if (obs_video_active()) { - ui->outputMode->setEnabled(false); - ui->outputModeLabel->setEnabled(false); - ui->simpleOutStrEncoderLabel->setEnabled(false); - ui->simpleOutStrEncoder->setEnabled(false); - ui->simpleOutStrAEncoderLabel->setEnabled(false); - ui->simpleOutStrAEncoder->setEnabled(false); - ui->simpleRecordingGroupBox->setEnabled(false); - ui->simpleReplayBuf->setEnabled(false); - ui->advOutTopContainer->setEnabled(false); - ui->advOutRecTopContainer->setEnabled(false); - ui->advOutRecTypeContainer->setEnabled(false); - ui->advOutputAudioTracksTab->setEnabled(false); - ui->advNetworkGroupBox->setEnabled(false); - } - - loading = false; -} - -void OBSBasicSettings::SetAdvOutputFFmpegEnablement(FFmpegCodecType encoderType, bool enabled, bool enableEncoder) -{ - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - - switch (encoderType) { - case FFmpegCodecType::VIDEO: - ui->advOutFFVBitrate->setEnabled(enabled); - ui->advOutFFVGOPSize->setEnabled(enabled); - ui->advOutFFUseRescale->setEnabled(enabled); - ui->advOutFFRescale->setEnabled(enabled && rescale); - ui->advOutFFVEncoder->setEnabled(enabled || enableEncoder); - ui->advOutFFVCfg->setEnabled(enabled); - break; - case FFmpegCodecType::AUDIO: - ui->advOutFFABitrate->setEnabled(enabled); - ui->advOutFFAEncoder->setEnabled(enabled || enableEncoder); - ui->advOutFFACfg->setEnabled(enabled); - ui->advOutFFTrack1->setEnabled(enabled); - ui->advOutFFTrack2->setEnabled(enabled); - ui->advOutFFTrack3->setEnabled(enabled); - ui->advOutFFTrack4->setEnabled(enabled); - ui->advOutFFTrack5->setEnabled(enabled); - ui->advOutFFTrack6->setEnabled(enabled); - default: - break; - } -} - -static inline void LoadListValue(QComboBox *widget, const char *text, const char *val) -{ - widget->addItem(QT_UTF8(text), QT_UTF8(val)); -} - -void OBSBasicSettings::LoadListValues(QComboBox *widget, obs_property_t *prop, int index) -{ - size_t count = obs_property_list_item_count(prop); - - OBSSourceAutoRelease source = obs_get_output_source(index); - const char *deviceId = nullptr; - OBSDataAutoRelease settings = nullptr; - - if (source) { - settings = obs_source_get_settings(source); - if (settings) - deviceId = obs_data_get_string(settings, "device_id"); - } - - widget->addItem(QTStr("Basic.Settings.Audio.Disabled"), "disabled"); - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(prop, i); - const char *val = obs_property_list_item_string(prop, i); - LoadListValue(widget, name, val); - } - - if (deviceId) { - QVariant var(QT_UTF8(deviceId)); - int idx = widget->findData(var); - if (idx != -1) { - widget->setCurrentIndex(idx); - } else { - widget->insertItem(0, - QTStr("Basic.Settings.Audio." - "UnknownAudioDevice"), - var); - widget->setCurrentIndex(0); - HighlightGroupBoxLabel(ui->audioDevicesGroupBox, widget, "errorLabel"); - } - } -} - -void OBSBasicSettings::LoadAudioDevices() -{ - const char *input_id = App()->InputAudioSource(); - const char *output_id = App()->OutputAudioSource(); - - obs_properties_t *input_props = obs_get_source_properties(input_id); - obs_properties_t *output_props = obs_get_source_properties(output_id); - - if (input_props) { - obs_property_t *inputs = obs_properties_get(input_props, "device_id"); - LoadListValues(ui->auxAudioDevice1, inputs, 3); - LoadListValues(ui->auxAudioDevice2, inputs, 4); - LoadListValues(ui->auxAudioDevice3, inputs, 5); - LoadListValues(ui->auxAudioDevice4, inputs, 6); - obs_properties_destroy(input_props); - } - - if (output_props) { - obs_property_t *outputs = obs_properties_get(output_props, "device_id"); - LoadListValues(ui->desktopAudioDevice1, outputs, 1); - LoadListValues(ui->desktopAudioDevice2, outputs, 2); - obs_properties_destroy(output_props); - } - - if (obs_video_active()) { - ui->sampleRate->setEnabled(false); - ui->channelSetup->setEnabled(false); - } -} - -#define NBSP "\xC2\xA0" - -void OBSBasicSettings::LoadAudioSources() -{ - if (ui->audioSourceLayout->rowCount() > 0) { - QLayoutItem *forDeletion = ui->audioSourceLayout->takeAt(0); - forDeletion->widget()->deleteLater(); - delete forDeletion; - } - auto layout = new QFormLayout(); - layout->setVerticalSpacing(15); - layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - - audioSourceSignals.clear(); - audioSources.clear(); - - auto widget = new QWidget(); - widget->setLayout(layout); - ui->audioSourceLayout->addRow(widget); - - const char *enablePtm = Str("Basic.Settings.Audio.EnablePushToMute"); - const char *ptmDelay = Str("Basic.Settings.Audio.PushToMuteDelay"); - const char *enablePtt = Str("Basic.Settings.Audio.EnablePushToTalk"); - const char *pttDelay = Str("Basic.Settings.Audio.PushToTalkDelay"); - auto AddSource = [&](obs_source_t *source) { - if (!(obs_source_get_output_flags(source) & OBS_SOURCE_AUDIO)) - return true; - - auto form = new QFormLayout(); - form->setVerticalSpacing(0); - form->setHorizontalSpacing(5); - form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - - auto ptmCB = new SilentUpdateCheckBox(); - ptmCB->setText(enablePtm); - ptmCB->setChecked(obs_source_push_to_mute_enabled(source)); - form->addRow(ptmCB); - - auto ptmSB = new SilentUpdateSpinBox(); - ptmSB->setSuffix(NBSP "ms"); - ptmSB->setRange(0, INT_MAX); - ptmSB->setValue(obs_source_get_push_to_mute_delay(source)); - form->addRow(ptmDelay, ptmSB); - - auto pttCB = new SilentUpdateCheckBox(); - pttCB->setText(enablePtt); - pttCB->setChecked(obs_source_push_to_talk_enabled(source)); - form->addRow(pttCB); - - auto pttSB = new SilentUpdateSpinBox(); - pttSB->setSuffix(NBSP "ms"); - pttSB->setRange(0, INT_MAX); - pttSB->setValue(obs_source_get_push_to_talk_delay(source)); - form->addRow(pttDelay, pttSB); - - HookWidget(ptmCB, CHECK_CHANGED, AUDIO_CHANGED); - HookWidget(ptmSB, SCROLL_CHANGED, AUDIO_CHANGED); - HookWidget(pttCB, CHECK_CHANGED, AUDIO_CHANGED); - HookWidget(pttSB, SCROLL_CHANGED, AUDIO_CHANGED); - - audioSourceSignals.reserve(audioSourceSignals.size() + 4); - - auto handler = obs_source_get_signal_handler(source); - audioSourceSignals.emplace_back( - handler, "push_to_mute_changed", - [](void *data, calldata_t *param) { - QMetaObject::invokeMethod(static_cast(data), "setCheckedSilently", - Q_ARG(bool, calldata_bool(param, "enabled"))); - }, - ptmCB); - audioSourceSignals.emplace_back( - handler, "push_to_mute_delay", - [](void *data, calldata_t *param) { - QMetaObject::invokeMethod(static_cast(data), "setValueSilently", - Q_ARG(int, calldata_int(param, "delay"))); - }, - ptmSB); - audioSourceSignals.emplace_back( - handler, "push_to_talk_changed", - [](void *data, calldata_t *param) { - QMetaObject::invokeMethod(static_cast(data), "setCheckedSilently", - Q_ARG(bool, calldata_bool(param, "enabled"))); - }, - pttCB); - audioSourceSignals.emplace_back( - handler, "push_to_talk_delay", - [](void *data, calldata_t *param) { - QMetaObject::invokeMethod(static_cast(data), "setValueSilently", - Q_ARG(int, calldata_int(param, "delay"))); - }, - pttSB); - - audioSources.emplace_back(OBSGetWeakRef(source), ptmCB, ptmSB, pttCB, pttSB); - - auto label = new OBSSourceLabel(source); - TruncateLabel(label, label->text()); - label->setMinimumSize(QSize(170, 0)); - label->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter); - connect(label, &OBSSourceLabel::Removed, - [=]() { QMetaObject::invokeMethod(this, "ReloadAudioSources"); }); - connect(label, &OBSSourceLabel::Destroyed, - [=]() { QMetaObject::invokeMethod(this, "ReloadAudioSources"); }); - - layout->addRow(label, form); - return true; - }; - - using AddSource_t = decltype(AddSource); - obs_enum_sources( - [](void *data, obs_source_t *source) { - auto &AddSource = *static_cast(data); - if (!obs_source_removed(source)) - AddSource(source); - return true; - }, - static_cast(&AddSource)); - - if (layout->rowCount() == 0) - ui->audioHotkeysGroupBox->hide(); - else - ui->audioHotkeysGroupBox->show(); -} - -void OBSBasicSettings::LoadAudioSettings() -{ - uint32_t sampleRate = config_get_uint(main->Config(), "Audio", "SampleRate"); - const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); - double meterDecayRate = config_get_double(main->Config(), "Audio", "MeterDecayRate"); - uint32_t peakMeterTypeIdx = config_get_uint(main->Config(), "Audio", "PeakMeterType"); - bool enableLLAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - - loading = true; - - const char *str; - if (sampleRate == 48000) - str = "48 kHz"; - else - str = "44.1 kHz"; - - int sampleRateIdx = ui->sampleRate->findText(str); - if (sampleRateIdx != -1) - ui->sampleRate->setCurrentIndex(sampleRateIdx); - - if (strcmp(speakers, "Mono") == 0) - ui->channelSetup->setCurrentIndex(0); - else if (strcmp(speakers, "2.1") == 0) - ui->channelSetup->setCurrentIndex(2); - else if (strcmp(speakers, "4.0") == 0) - ui->channelSetup->setCurrentIndex(3); - else if (strcmp(speakers, "4.1") == 0) - ui->channelSetup->setCurrentIndex(4); - else if (strcmp(speakers, "5.1") == 0) - ui->channelSetup->setCurrentIndex(5); - else if (strcmp(speakers, "7.1") == 0) - ui->channelSetup->setCurrentIndex(6); - else - ui->channelSetup->setCurrentIndex(1); - - if (meterDecayRate == VOLUME_METER_DECAY_MEDIUM) - ui->meterDecayRate->setCurrentIndex(1); - else if (meterDecayRate == VOLUME_METER_DECAY_SLOW) - ui->meterDecayRate->setCurrentIndex(2); - else - ui->meterDecayRate->setCurrentIndex(0); - - ui->peakMeterType->setCurrentIndex(peakMeterTypeIdx); - ui->lowLatencyBuffering->setChecked(enableLLAudioBuffering); - - LoadAudioDevices(); - LoadAudioSources(); - - loading = false; -} - -void OBSBasicSettings::UpdateColorFormatSpaceWarning() -{ - const QString format = ui->colorFormat->currentData().toString(); - switch (ui->colorSpace->currentIndex()) { - case 3: /* Rec.2100 (PQ) */ - case 4: /* Rec.2100 (HLG) */ - if ((format == "P010") || (format == "P216") || (format == "P416")) { - ui->advancedMsg2->clear(); - ui->advancedMsg2->setVisible(false); - } else if (format == "I010") { - ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning")); - ui->advancedMsg2->setVisible(true); - } else { - ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning2100")); - ui->advancedMsg2->setVisible(true); - } - break; - default: - if (format == "NV12") { - ui->advancedMsg2->clear(); - ui->advancedMsg2->setVisible(false); - } else if ((format == "I010") || (format == "P010") || (format == "P216") || (format == "P416")) { - ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarningPreciseSdr")); - ui->advancedMsg2->setVisible(true); - } else { - ui->advancedMsg2->setText(QTStr("Basic.Settings.Advanced.FormatWarning")); - ui->advancedMsg2->setVisible(true); - } - } -} - -void OBSBasicSettings::LoadAdvancedSettings() -{ - const char *videoColorFormat = config_get_string(main->Config(), "Video", "ColorFormat"); - const char *videoColorSpace = config_get_string(main->Config(), "Video", "ColorSpace"); - const char *videoColorRange = config_get_string(main->Config(), "Video", "ColorRange"); - uint32_t sdrWhiteLevel = (uint32_t)config_get_uint(main->Config(), "Video", "SdrWhiteLevel"); - uint32_t hdrNominalPeakLevel = (uint32_t)config_get_uint(main->Config(), "Video", "HdrNominalPeakLevel"); - - QString monDevName; - QString monDevId; - if (obs_audio_monitoring_available()) { - monDevName = config_get_string(main->Config(), "Audio", "MonitoringDeviceName"); - monDevId = config_get_string(main->Config(), "Audio", "MonitoringDeviceId"); - } - bool enableDelay = 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"); - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); - const char *filename = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - bool replayBuf = config_get_bool(main->Config(), "AdvOut", "RecRB"); - int rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - const char *hotkeyFocusType = config_get_string(App()->GetUserConfig(), "General", "HotkeyFocusType"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); - bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); - - loading = true; - - LoadRendererList(); - - if (obs_audio_monitoring_available() && !SetComboByValue(ui->monitoringDevice, monDevId.toUtf8())) - SetInvalidValue(ui->monitoringDevice, monDevName.toUtf8(), monDevId.toUtf8()); - - ui->confirmOnExit->setChecked(confirmOnExit); - - ui->filenameFormatting->setText(filename); - ui->overwriteIfExists->setChecked(overwriteIfExists); - ui->simpleRBPrefix->setText(rbPrefix); - ui->simpleRBSuffix->setText(rbSuffix); - - ui->advReplayBuf->setChecked(replayBuf); - ui->advRBSecMax->setValue(rbTime); - ui->advRBMegsMax->setValue(rbSize); - - ui->reconnectEnable->setChecked(reconnect); - ui->reconnectRetryDelay->setValue(retryDelay); - ui->reconnectMaxRetries->setValue(maxRetries); - - ui->streamDelaySec->setValue(delaySec); - ui->streamDelayPreserve->setChecked(preserveDelay); - ui->streamDelayEnable->setChecked(enableDelay); - ui->autoRemux->setChecked(autoRemux); - ui->dynBitrate->setChecked(dynBitrate); - - SetComboByValue(ui->colorFormat, videoColorFormat); - SetComboByValue(ui->colorSpace, videoColorSpace); - SetComboByValue(ui->colorRange, videoColorRange); - ui->sdrWhiteLevel->setValue(sdrWhiteLevel); - ui->hdrNominalPeakLevel->setValue(hdrNominalPeakLevel); - - SetComboByValue(ui->ipFamily, ipFamily); - if (!SetComboByValue(ui->bindToIP, bindIP)) - SetInvalidValue(ui->bindToIP, bindIP, bindIP); - - if (obs_video_active()) { - ui->advancedVideoContainer->setEnabled(false); - } - -#ifdef __APPLE__ - bool disableOSXVSync = config_get_bool(App()->GetAppConfig(), "Video", "DisableOSXVSync"); - bool resetOSXVSync = config_get_bool(App()->GetAppConfig(), "Video", "ResetOSXVSyncOnExit"); - ui->disableOSXVSync->setChecked(disableOSXVSync); - ui->resetOSXVSync->setChecked(resetOSXVSync); - ui->resetOSXVSync->setEnabled(disableOSXVSync); -#elif _WIN32 - bool disableAudioDucking = config_get_bool(App()->GetAppConfig(), "Audio", "DisableAudioDucking"); - ui->disableAudioDucking->setChecked(disableAudioDucking); - - const char *processPriority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); - - int idx = ui->processPriority->findData(processPriority); - if (idx == -1) - idx = ui->processPriority->findData("Normal"); - ui->processPriority->setCurrentIndex(idx); - - ui->enableNewSocketLoop->setChecked(enableNewSocketLoop); - ui->enableLowLatencyMode->setChecked(enableLowLatencyMode); - ui->enableLowLatencyMode->setToolTip(QTStr("Basic.Settings.Advanced.Network.TCPPacing.Tooltip")); -#endif -#if defined(_WIN32) || defined(__APPLE__) - bool browserHWAccel = config_get_bool(App()->GetAppConfig(), "General", "BrowserHWAccel"); - ui->browserHWAccel->setChecked(browserHWAccel); - prevBrowserAccel = ui->browserHWAccel->isChecked(); -#endif - - SetComboByValue(ui->hotkeyFocusType, hotkeyFocusType); - - loading = false; -} - -template -static inline void LayoutHotkey(OBSBasicSettings *settings, obs_hotkey_id id, obs_hotkey_t *key, Func &&fun, - const map> &keys) -{ - auto *label = new OBSHotkeyLabel; - QString text = QT_UTF8(obs_hotkey_get_description(key)); - - label->setProperty("fullName", text); - TruncateLabel(label, text); - - OBSHotkeyWidget *hw = nullptr; - - auto combos = keys.find(id); - if (combos == std::end(keys)) - hw = new OBSHotkeyWidget(settings, id, obs_hotkey_get_name(key), settings); - else - hw = new OBSHotkeyWidget(settings, id, obs_hotkey_get_name(key), settings, combos->second); - - hw->label = label; - hw->setAccessibleName(text); - label->widget = hw; - - fun(key, label, hw); -} - -template static QLabel *makeLabel(T &t, Func &&getName) -{ - QLabel *label = new QLabel(getName(t)); - label->setStyleSheet("font-weight: bold;"); - return label; -} - -template static QLabel *makeLabel(const OBSSource &source, Func &&) -{ - OBSSourceLabel *label = new OBSSourceLabel(source); - label->setStyleSheet("font-weight: bold;"); - QString name = QT_UTF8(obs_source_get_name(source)); - TruncateLabel(label, name); - - return label; -} - -template -static inline void AddHotkeys(QFormLayout &layout, Func &&getName, - std::vector, QPointer>> &hotkeys) -{ - if (hotkeys.empty()) - return; - - layout.setItem(layout.rowCount(), QFormLayout::SpanningRole, new QSpacerItem(0, 10)); - - using tuple_type = std::tuple, QPointer>; - - stable_sort(begin(hotkeys), end(hotkeys), [&](const tuple_type &a, const tuple_type &b) { - const auto &o_a = get<0>(a); - const auto &o_b = get<0>(b); - return o_a != o_b && string(getName(o_a)) < getName(o_b); - }); - - string prevName; - for (const auto &hotkey : hotkeys) { - const auto &o = get<0>(hotkey); - const char *name = getName(o); - if (prevName != name) { - prevName = name; - layout.setItem(layout.rowCount(), QFormLayout::SpanningRole, new QSpacerItem(0, 10)); - layout.addRow(makeLabel(o, getName)); - } - - auto hlabel = get<1>(hotkey); - auto widget = get<2>(hotkey); - layout.addRow(hlabel, widget); - } -} - -void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey) -{ - hotkeys.clear(); - if (ui->hotkeyFormLayout->rowCount() > 0) { - QLayoutItem *forDeletion = ui->hotkeyFormLayout->takeAt(0); - forDeletion->widget()->deleteLater(); - delete forDeletion; - } - ui->hotkeyFilterSearch->blockSignals(true); - ui->hotkeyFilterInput->blockSignals(true); - ui->hotkeyFilterSearch->setText(""); - ui->hotkeyFilterInput->ResetKey(); - ui->hotkeyFilterSearch->blockSignals(false); - ui->hotkeyFilterInput->blockSignals(false); - - using keys_t = map>; - keys_t keys; - obs_enum_hotkey_bindings( - [](void *data, size_t, obs_hotkey_binding_t *binding) { - auto &keys = *static_cast(data); - - keys[obs_hotkey_binding_get_hotkey_id(binding)].emplace_back( - obs_hotkey_binding_get_key_combination(binding)); - - return true; - }, - &keys); - - QFormLayout *hotkeysLayout = new QFormLayout(); - hotkeysLayout->setVerticalSpacing(0); - hotkeysLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - hotkeysLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter); - auto hotkeyChildWidget = new QWidget(); - hotkeyChildWidget->setLayout(hotkeysLayout); - ui->hotkeyFormLayout->addRow(hotkeyChildWidget); - - using namespace std; - using encoders_elem_t = tuple, QPointer>; - using outputs_elem_t = tuple, QPointer>; - using services_elem_t = tuple, QPointer>; - using sources_elem_t = tuple, QPointer>; - vector encoders; - vector outputs; - vector services; - vector scenes; - vector sources; - - vector pairIds; - map> pairLabels; - - using std::move; - - auto HandleEncoder = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { - auto weak_encoder = static_cast(registerer); - auto encoder = OBSGetStrongRef(weak_encoder); - - if (!encoder) - return true; - - encoders.emplace_back(std::move(encoder), label, hw); - return false; - }; - - auto HandleOutput = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { - auto weak_output = static_cast(registerer); - auto output = OBSGetStrongRef(weak_output); - - if (!output) - return true; - - outputs.emplace_back(std::move(output), label, hw); - return false; - }; - - auto HandleService = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { - auto weak_service = static_cast(registerer); - auto service = OBSGetStrongRef(weak_service); - - if (!service) - return true; - - services.emplace_back(std::move(service), label, hw); - return false; - }; - - auto HandleSource = [&](void *registerer, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { - auto weak_source = static_cast(registerer); - auto source = OBSGetStrongRef(weak_source); - - if (!source) - return true; - - if (obs_scene_from_source(source)) - scenes.emplace_back(source, label, hw); - else if (obs_source_get_name(source) != NULL) - sources.emplace_back(source, label, hw); - - return false; - }; - - auto RegisterHotkey = [&](obs_hotkey_t *key, OBSHotkeyLabel *label, OBSHotkeyWidget *hw) { - auto registerer_type = obs_hotkey_get_registerer_type(key); - void *registerer = obs_hotkey_get_registerer(key); - - obs_hotkey_id partner = obs_hotkey_get_pair_partner_id(key); - if (partner != OBS_INVALID_HOTKEY_ID) { - pairLabels.emplace(obs_hotkey_get_id(key), make_pair(partner, label)); - pairIds.push_back(obs_hotkey_get_id(key)); - } - - using std::move; - - switch (registerer_type) { - case OBS_HOTKEY_REGISTERER_FRONTEND: - hotkeysLayout->addRow(label, hw); - break; - - case OBS_HOTKEY_REGISTERER_ENCODER: - if (HandleEncoder(registerer, label, hw)) - return; - break; - - case OBS_HOTKEY_REGISTERER_OUTPUT: - if (HandleOutput(registerer, label, hw)) - return; - break; - - case OBS_HOTKEY_REGISTERER_SERVICE: - if (HandleService(registerer, label, hw)) - return; - break; - - case OBS_HOTKEY_REGISTERER_SOURCE: - if (HandleSource(registerer, label, hw)) - return; - break; - } - - hotkeys.emplace_back(registerer_type == OBS_HOTKEY_REGISTERER_FRONTEND, hw); - connect(hw, &OBSHotkeyWidget::KeyChanged, this, [=]() { - HotkeysChanged(); - ScanDuplicateHotkeys(hotkeysLayout); - }); - connect(hw, &OBSHotkeyWidget::SearchKey, [=](obs_key_combination_t combo) { - ui->hotkeyFilterSearch->setText(""); - ui->hotkeyFilterInput->HandleNewKey(combo); - ui->hotkeyFilterInput->KeyChanged(combo); - }); - }; - - auto data = make_tuple(RegisterHotkey, std::move(keys), ignoreKey, this); - using data_t = decltype(data); - obs_enum_hotkeys( - [](void *data, obs_hotkey_id id, obs_hotkey_t *key) { - data_t &d = *static_cast(data); - if (id != get<2>(d)) - LayoutHotkey(get<3>(d), id, key, get<0>(d), get<1>(d)); - return true; - }, - &data); - - for (auto keyId : pairIds) { - auto data1 = pairLabels.find(keyId); - if (data1 == end(pairLabels)) - continue; - - auto &label1 = data1->second.second; - if (label1->pairPartner) - continue; - - auto data2 = pairLabels.find(data1->second.first); - if (data2 == end(pairLabels)) - continue; - - auto &label2 = data2->second.second; - if (label2->pairPartner) - continue; - - QString tt = QTStr("Basic.Settings.Hotkeys.Pair"); - auto name1 = label1->text(); - auto name2 = label2->text(); - - auto Update = [&](OBSHotkeyLabel *label, const QString &name, OBSHotkeyLabel *other, - const QString &otherName) { - QString string = other->property("fullName").value(); - - if (string.isEmpty() || string.isNull()) - string = otherName; - - label->setToolTip(tt.arg(string)); - label->setText(name + " *"); - label->pairPartner = other; - }; - Update(label1, name1, label2, name2); - Update(label2, name2, label1, name1); - } - - AddHotkeys(*hotkeysLayout, obs_output_get_name, outputs); - AddHotkeys(*hotkeysLayout, obs_source_get_name, scenes); - AddHotkeys(*hotkeysLayout, obs_source_get_name, sources); - AddHotkeys(*hotkeysLayout, obs_encoder_get_name, encoders); - AddHotkeys(*hotkeysLayout, obs_service_get_name, services); - - ScanDuplicateHotkeys(hotkeysLayout); - - /* After this function returns the UI can still be unresponsive for a bit. - * So by deferring the call to unsetCursor() to the Qt event loop it will - * take until it has actually finished processing the created widgets - * before the cursor is reset. */ - QTimer::singleShot(1, this, &OBSBasicSettings::unsetCursor); - hotkeysLoaded = true; -} - -void OBSBasicSettings::LoadSettings(bool changedOnly) -{ - if (!changedOnly || generalChanged) - LoadGeneralSettings(); - if (!changedOnly || stream1Changed) - LoadStream1Settings(); - if (!changedOnly || outputsChanged) - LoadOutputSettings(); - if (!changedOnly || audioChanged) - LoadAudioSettings(); - if (!changedOnly || videoChanged) - LoadVideoSettings(); - if (!changedOnly || a11yChanged) - LoadA11ySettings(); - if (!changedOnly || appearanceChanged) - LoadAppearanceSettings(); - if (!changedOnly || advancedChanged) - LoadAdvancedSettings(); -} - -void OBSBasicSettings::SaveGeneralSettings() -{ - int languageIndex = ui->language->currentIndex(); - QVariant langData = ui->language->itemData(languageIndex); - string language = langData.toString().toStdString(); - - if (WidgetChanged(ui->language)) - config_set_string(App()->GetUserConfig(), "General", "Language", language.c_str()); - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - if (WidgetChanged(ui->enableAutoUpdates)) - config_set_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates", - ui->enableAutoUpdates->isChecked()); - int branchIdx = ui->updateChannelBox->currentIndex(); - QString branchName = ui->updateChannelBox->itemData(branchIdx).toString(); - - if (WidgetChanged(ui->updateChannelBox)) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", QT_TO_UTF8(branchName)); - forceUpdateCheck = true; - } -#endif -#ifdef _WIN32 - if (ui->hideOBSFromCapture && WidgetChanged(ui->hideOBSFromCapture)) { - bool hide_window = ui->hideOBSFromCapture->isChecked(); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture", hide_window); - - QWindowList windows = QGuiApplication::allWindows(); - for (auto window : windows) { - if (window->isVisible()) { - main->SetDisplayAffinity(window); - } - } - - blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hide_window ? "true" : "false"); - } -#endif - if (WidgetChanged(ui->openStatsOnStartup)) - config_set_bool(main->Config(), "General", "OpenStatsOnStartup", ui->openStatsOnStartup->isChecked()); - if (WidgetChanged(ui->snappingEnabled)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SnappingEnabled", - ui->snappingEnabled->isChecked()); - if (WidgetChanged(ui->screenSnapping)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ScreenSnapping", - ui->screenSnapping->isChecked()); - if (WidgetChanged(ui->centerSnapping)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "CenterSnapping", - ui->centerSnapping->isChecked()); - if (WidgetChanged(ui->sourceSnapping)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SourceSnapping", - ui->sourceSnapping->isChecked()); - if (WidgetChanged(ui->snapDistance)) - config_set_double(App()->GetUserConfig(), "BasicWindow", "SnapDistance", ui->snapDistance->value()); - if (WidgetChanged(ui->overflowAlwaysVisible) || WidgetChanged(ui->overflowHide) || - WidgetChanged(ui->overflowSelectionHide)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible", - ui->overflowAlwaysVisible->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden", ui->overflowHide->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden", - ui->overflowSelectionHide->isChecked()); - main->UpdatePreviewOverflowSettings(); - } - if (WidgetChanged(ui->previewSafeAreas)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas", - ui->previewSafeAreas->isChecked()); - main->UpdatePreviewSafeAreas(); - } - - if (WidgetChanged(ui->previewSpacingHelpers)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled", - ui->previewSpacingHelpers->isChecked()); - main->UpdatePreviewSpacingHelpers(); - } - - if (WidgetChanged(ui->doubleClickSwitch)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick", - ui->doubleClickSwitch->isChecked()); - if (WidgetChanged(ui->automaticSearch)) - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", - ui->automaticSearch->isChecked()); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream", - ui->warnBeforeStreamStart->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream", - ui->warnBeforeStreamStop->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord", - ui->warnBeforeRecordStop->isChecked()); - - if (WidgetChanged(ui->hideProjectorCursor)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "HideProjectorCursor", - ui->hideProjectorCursor->isChecked()); - main->UpdateProjectorHideCursor(); - } - - if (WidgetChanged(ui->projectorAlwaysOnTop)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ProjectorAlwaysOnTop", - ui->projectorAlwaysOnTop->isChecked()); -#if defined(_WIN32) || defined(__APPLE__) - main->UpdateProjectorAlwaysOnTop(ui->projectorAlwaysOnTop->isChecked()); -#else - main->ResetProjectors(); -#endif - } - - if (WidgetChanged(ui->recordWhenStreaming)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming", - ui->recordWhenStreaming->isChecked()); - if (WidgetChanged(ui->keepRecordStreamStops)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops", - ui->keepRecordStreamStops->isChecked()); - - if (WidgetChanged(ui->replayWhileStreaming)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming", - ui->replayWhileStreaming->isChecked()); - if (WidgetChanged(ui->keepReplayStreamStops)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops", - ui->keepReplayStreamStops->isChecked()); - - if (WidgetChanged(ui->systemTrayEnabled)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled", - ui->systemTrayEnabled->isChecked()); - - main->SystemTray(false); - } - - if (WidgetChanged(ui->systemTrayWhenStarted)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted", - ui->systemTrayWhenStarted->isChecked()); - - if (WidgetChanged(ui->systemTrayAlways)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray", - ui->systemTrayAlways->isChecked()); - - if (WidgetChanged(ui->saveProjectors)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors", - ui->saveProjectors->isChecked()); - - if (WidgetChanged(ui->closeProjectors)) - config_set_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors", - ui->closeProjectors->isChecked()); - - if (WidgetChanged(ui->studioPortraitLayout)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout", - ui->studioPortraitLayout->isChecked()); - - main->ResetUI(); - } - - if (WidgetChanged(ui->prevProgLabelToggle)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels", - ui->prevProgLabelToggle->isChecked()); - - main->ResetUI(); - } - - bool multiviewChanged = false; - if (WidgetChanged(ui->multiviewMouseSwitch)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewMouseSwitch", - ui->multiviewMouseSwitch->isChecked()); - multiviewChanged = true; - } - - if (WidgetChanged(ui->multiviewDrawNames)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawNames", - ui->multiviewDrawNames->isChecked()); - multiviewChanged = true; - } - - if (WidgetChanged(ui->multiviewDrawAreas)) { - config_set_bool(App()->GetUserConfig(), "BasicWindow", "MultiviewDrawAreas", - ui->multiviewDrawAreas->isChecked()); - multiviewChanged = true; - } - - if (WidgetChanged(ui->multiviewLayout)) { - config_set_int(App()->GetUserConfig(), "BasicWindow", "MultiviewLayout", - ui->multiviewLayout->currentData().toInt()); - multiviewChanged = true; - } - - if (multiviewChanged) - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasicSettings::SaveVideoSettings() -{ - QString baseResolution = ui->baseResolution->currentText(); - QString outputResolution = ui->outputResolution->currentText(); - int fpsType = ui->fpsType->currentIndex(); - uint32_t cx = 0, cy = 0; - - /* ------------------- */ - - if (WidgetChanged(ui->baseResolution) && ConvertResText(QT_TO_UTF8(baseResolution), cx, cy)) { - config_set_uint(main->Config(), "Video", "BaseCX", cx); - config_set_uint(main->Config(), "Video", "BaseCY", cy); - } - - if (WidgetChanged(ui->outputResolution) && ConvertResText(QT_TO_UTF8(outputResolution), cx, cy)) { - config_set_uint(main->Config(), "Video", "OutputCX", cx); - config_set_uint(main->Config(), "Video", "OutputCY", cy); - } - - if (WidgetChanged(ui->fpsType)) - config_set_uint(main->Config(), "Video", "FPSType", fpsType); - - SaveCombo(ui->fpsCommon, "Video", "FPSCommon"); - SaveSpinBox(ui->fpsInteger, "Video", "FPSInt"); - SaveSpinBox(ui->fpsNumerator, "Video", "FPSNum"); - SaveSpinBox(ui->fpsDenominator, "Video", "FPSDen"); - SaveComboData(ui->downscaleFilter, "Video", "ScaleType"); -} - -void OBSBasicSettings::SaveAdvancedSettings() -{ - QString lastMonitoringDevice = config_get_string(main->Config(), "Audio", "MonitoringDeviceId"); - -#ifdef _WIN32 - if (WidgetChanged(ui->renderer)) - config_set_string(App()->GetAppConfig(), "Video", "Renderer", QT_TO_UTF8(ui->renderer->currentText())); - - std::string priority = QT_TO_UTF8(ui->processPriority->currentData().toString()); - config_set_string(App()->GetAppConfig(), "General", "ProcessPriority", priority.c_str()); - if (main->Active()) - SetProcessPriority(priority.c_str()); - - SaveCheckBox(ui->enableNewSocketLoop, "Output", "NewSocketLoopEnable"); - SaveCheckBox(ui->enableLowLatencyMode, "Output", "LowLatencyEnable"); -#endif -#if defined(_WIN32) || defined(__APPLE__) - bool browserHWAccel = ui->browserHWAccel->isChecked(); - config_set_bool(App()->GetAppConfig(), "General", "BrowserHWAccel", browserHWAccel); -#endif - - if (WidgetChanged(ui->hotkeyFocusType)) { - QString str = GetComboData(ui->hotkeyFocusType); - config_set_string(App()->GetUserConfig(), "General", "HotkeyFocusType", QT_TO_UTF8(str)); - } - -#ifdef __APPLE__ - if (WidgetChanged(ui->disableOSXVSync)) { - bool disable = ui->disableOSXVSync->isChecked(); - config_set_bool(App()->GetAppConfig(), "Video", "DisableOSXVSync", disable); - EnableOSXVSync(!disable); - } - if (WidgetChanged(ui->resetOSXVSync)) - config_set_bool(App()->GetAppConfig(), "Video", "ResetOSXVSyncOnExit", ui->resetOSXVSync->isChecked()); -#endif - - SaveComboData(ui->colorFormat, "Video", "ColorFormat"); - SaveComboData(ui->colorSpace, "Video", "ColorSpace"); - SaveComboData(ui->colorRange, "Video", "ColorRange"); - SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel"); - SaveSpinBox(ui->hdrNominalPeakLevel, "Video", "HdrNominalPeakLevel"); - if (obs_audio_monitoring_available()) { - SaveCombo(ui->monitoringDevice, "Audio", "MonitoringDeviceName"); - SaveComboData(ui->monitoringDevice, "Audio", "MonitoringDeviceId"); - } - -#ifdef _WIN32 - if (WidgetChanged(ui->disableAudioDucking)) { - bool disable = ui->disableAudioDucking->isChecked(); - config_set_bool(App()->GetAppConfig(), "Audio", "DisableAudioDucking", disable); - DisableAudioDucking(disable); - } -#endif - - if (WidgetChanged(ui->confirmOnExit)) - config_set_bool(App()->GetUserConfig(), "General", "ConfirmOnExit", ui->confirmOnExit->isChecked()); - - SaveEdit(ui->filenameFormatting, "Output", "FilenameFormatting"); - SaveEdit(ui->simpleRBPrefix, "SimpleOutput", "RecRBPrefix"); - SaveEdit(ui->simpleRBSuffix, "SimpleOutput", "RecRBSuffix"); - SaveCheckBox(ui->overwriteIfExists, "Output", "OverwriteIfExists"); - SaveCheckBox(ui->streamDelayEnable, "Output", "DelayEnable"); - SaveSpinBox(ui->streamDelaySec, "Output", "DelaySec"); - SaveCheckBox(ui->streamDelayPreserve, "Output", "DelayPreserve"); - SaveCheckBox(ui->reconnectEnable, "Output", "Reconnect"); - SaveSpinBox(ui->reconnectRetryDelay, "Output", "RetryDelay"); - SaveSpinBox(ui->reconnectMaxRetries, "Output", "MaxRetries"); - SaveComboData(ui->bindToIP, "Output", "BindIP"); - SaveComboData(ui->ipFamily, "Output", "IPFamily"); - SaveCheckBox(ui->autoRemux, "Video", "AutoRemux"); - SaveCheckBox(ui->dynBitrate, "Output", "DynamicBitrate"); - - if (obs_audio_monitoring_available()) { - QString newDevice = ui->monitoringDevice->currentData().toString(); - - if (lastMonitoringDevice != newDevice) { - obs_set_audio_monitoring_device(QT_TO_UTF8(ui->monitoringDevice->currentText()), - QT_TO_UTF8(newDevice)); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", - QT_TO_UTF8(ui->monitoringDevice->currentText()), QT_TO_UTF8(newDevice)); - } - } -} - -static inline const char *OutputModeFromIdx(int idx) -{ - if (idx == 1) - return "Advanced"; - else - return "Simple"; -} - -static inline const char *RecTypeFromIdx(int idx) -{ - if (idx == 1) - return "FFmpeg"; - else - return "Standard"; -} - -static inline const char *SplitFileTypeFromIdx(int idx) -{ - if (idx == 1) - return "Size"; - else if (idx == 2) - return "Manual"; - else - return "Time"; -} - -static void WriteJsonData(OBSPropertiesView *view, const char *path) -{ - if (!view || !WidgetChanged(view)) - return; - - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(path); - - if (!jsonFilePath.empty()) { - obs_data_t *settings = view->GetSettings(); - if (settings) { - obs_data_save_json_safe(settings, jsonFilePath.u8string().c_str(), "tmp", "bak"); - } - } -} - -static void SaveTrackIndex(config_t *config, const char *section, const char *name, QAbstractButton *check1, - QAbstractButton *check2, QAbstractButton *check3, QAbstractButton *check4, - QAbstractButton *check5, QAbstractButton *check6) -{ - if (check1->isChecked()) - config_set_int(config, section, name, 1); - else if (check2->isChecked()) - config_set_int(config, section, name, 2); - else if (check3->isChecked()) - config_set_int(config, section, name, 3); - else if (check4->isChecked()) - config_set_int(config, section, name, 4); - else if (check5->isChecked()) - config_set_int(config, section, name, 5); - else if (check6->isChecked()) - config_set_int(config, section, name, 6); -} - -void OBSBasicSettings::SaveFormat(QComboBox *combo) -{ - QVariant v = combo->currentData(); - if (!v.isNull()) { - auto format = v.value(); - config_set_string(main->Config(), "AdvOut", "FFFormat", format.name); - config_set_string(main->Config(), "AdvOut", "FFFormatMimeType", format.mime_type); - - const char *ext = format.extensions; - string extStr = ext ? ext : ""; - - char *comma = strchr(&extStr[0], ','); - if (comma) - *comma = 0; - - config_set_string(main->Config(), "AdvOut", "FFExtension", extStr.c_str()); - } else { - config_set_string(main->Config(), "AdvOut", "FFFormat", nullptr); - config_set_string(main->Config(), "AdvOut", "FFFormatMimeType", nullptr); - - config_remove_value(main->Config(), "AdvOut", "FFExtension"); - } -} - -void OBSBasicSettings::SaveEncoder(QComboBox *combo, const char *section, const char *value) -{ - QVariant v = combo->currentData(); - FFmpegCodec cd{}; - if (!v.isNull()) - cd = v.value(); - - config_set_int(main->Config(), section, QT_TO_UTF8(QString("%1Id").arg(value)), cd.id); - if (cd.id != 0) - config_set_string(main->Config(), section, value, cd.name); - else - config_set_string(main->Config(), section, value, nullptr); -} - -void OBSBasicSettings::SaveOutputSettings() -{ - config_set_string(main->Config(), "Output", "Mode", OutputModeFromIdx(ui->outputMode->currentIndex())); - - QString encoder = ui->simpleOutStrEncoder->currentData().toString(); - const char *presetType; - - if (encoder == SIMPLE_ENCODER_QSV) - presetType = "QSVPreset"; - else if (encoder == SIMPLE_ENCODER_QSV_AV1) - presetType = "QSVPreset"; - else if (encoder == SIMPLE_ENCODER_NVENC) - presetType = "NVENCPreset2"; - else if (encoder == SIMPLE_ENCODER_NVENC_AV1) - presetType = "NVENCPreset2"; -#ifdef ENABLE_HEVC - else if (encoder == SIMPLE_ENCODER_AMD_HEVC) - presetType = "AMDPreset"; - else if (encoder == SIMPLE_ENCODER_NVENC_HEVC) - presetType = "NVENCPreset2"; -#endif - else if (encoder == SIMPLE_ENCODER_AMD) - presetType = "AMDPreset"; - else if (encoder == SIMPLE_ENCODER_AMD_AV1) - presetType = "AMDAV1Preset"; - else if (encoder == SIMPLE_ENCODER_APPLE_H264 -#ifdef ENABLE_HEVC - || encoder == SIMPLE_ENCODER_APPLE_HEVC -#endif - ) - /* The Apple encoders don't have presets like the other encoders - do. This only exists to make sure that the x264 preset doesn't - get overwritten with empty data. */ - presetType = "ApplePreset"; - else - presetType = "Preset"; - - SaveSpinBox(ui->simpleOutputVBitrate, "SimpleOutput", "VBitrate"); - SaveComboData(ui->simpleOutStrEncoder, "SimpleOutput", "StreamEncoder"); - SaveComboData(ui->simpleOutStrAEncoder, "SimpleOutput", "StreamAudioEncoder"); - SaveCombo(ui->simpleOutputABitrate, "SimpleOutput", "ABitrate"); - SaveEdit(ui->simpleOutputPath, "SimpleOutput", "FilePath"); - SaveCheckBox(ui->simpleNoSpace, "SimpleOutput", "FileNameWithoutSpace"); - SaveComboData(ui->simpleOutRecFormat, "SimpleOutput", "RecFormat2"); - SaveCheckBox(ui->simpleOutAdvanced, "SimpleOutput", "UseAdvanced"); - SaveComboData(ui->simpleOutPreset, "SimpleOutput", presetType); - SaveEdit(ui->simpleOutCustom, "SimpleOutput", "x264Settings"); - SaveComboData(ui->simpleOutRecQuality, "SimpleOutput", "RecQuality"); - SaveComboData(ui->simpleOutRecEncoder, "SimpleOutput", "RecEncoder"); - SaveComboData(ui->simpleOutRecAEncoder, "SimpleOutput", "RecAudioEncoder"); - SaveEdit(ui->simpleOutMuxCustom, "SimpleOutput", "MuxerCustom"); - SaveGroupBox(ui->simpleReplayBuf, "SimpleOutput", "RecRB"); - SaveSpinBox(ui->simpleRBSecMax, "SimpleOutput", "RecRBTime"); - SaveSpinBox(ui->simpleRBMegsMax, "SimpleOutput", "RecRBSize"); - config_set_int(main->Config(), "SimpleOutput", "RecTracks", SimpleOutGetSelectedAudioTracks()); - - curAdvStreamEncoder = GetComboData(ui->advOutEncoder); - - SaveComboData(ui->advOutEncoder, "AdvOut", "Encoder"); - SaveComboData(ui->advOutAEncoder, "AdvOut", "AudioEncoder"); - SaveCombo(ui->advOutRescale, "AdvOut", "RescaleRes"); - SaveComboData(ui->advOutRescaleFilter, "AdvOut", "RescaleFilter"); - SaveTrackIndex(main->Config(), "AdvOut", "TrackIndex", ui->advOutTrack1, ui->advOutTrack2, ui->advOutTrack3, - ui->advOutTrack4, ui->advOutTrack5, ui->advOutTrack6); - config_set_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes", AdvOutGetStreamingSelectedAudioTracks()); - config_set_string(main->Config(), "AdvOut", "RecType", RecTypeFromIdx(ui->advOutRecType->currentIndex())); - - curAdvRecordEncoder = GetComboData(ui->advOutRecEncoder); - - SaveEdit(ui->advOutRecPath, "AdvOut", "RecFilePath"); - SaveCheckBox(ui->advOutNoSpace, "AdvOut", "RecFileNameWithoutSpace"); - SaveComboData(ui->advOutRecFormat, "AdvOut", "RecFormat2"); - SaveComboData(ui->advOutRecEncoder, "AdvOut", "RecEncoder"); - SaveComboData(ui->advOutRecAEncoder, "AdvOut", "RecAudioEncoder"); - SaveCombo(ui->advOutRecRescale, "AdvOut", "RecRescaleRes"); - SaveComboData(ui->advOutRecRescaleFilter, "AdvOut", "RecRescaleFilter"); - SaveEdit(ui->advOutMuxCustom, "AdvOut", "RecMuxerCustom"); - SaveCheckBox(ui->advOutSplitFile, "AdvOut", "RecSplitFile"); - config_set_string(main->Config(), "AdvOut", "RecSplitFileType", - SplitFileTypeFromIdx(ui->advOutSplitFileType->currentIndex())); - SaveSpinBox(ui->advOutSplitFileTime, "AdvOut", "RecSplitFileTime"); - SaveSpinBox(ui->advOutSplitFileSize, "AdvOut", "RecSplitFileSize"); - - config_set_int(main->Config(), "AdvOut", "RecTracks", AdvOutGetSelectedAudioTracks()); - - config_set_int(main->Config(), "AdvOut", "FLVTrack", CurrentFLVTrack()); - - config_set_bool(main->Config(), "AdvOut", "FFOutputToFile", - ui->advOutFFType->currentIndex() == 0 ? true : false); - SaveEdit(ui->advOutFFRecPath, "AdvOut", "FFFilePath"); - SaveCheckBox(ui->advOutFFNoSpace, "AdvOut", "FFFileNameWithoutSpace"); - SaveEdit(ui->advOutFFURL, "AdvOut", "FFURL"); - SaveFormat(ui->advOutFFFormat); - SaveEdit(ui->advOutFFMCfg, "AdvOut", "FFMCustom"); - SaveSpinBox(ui->advOutFFVBitrate, "AdvOut", "FFVBitrate"); - SaveSpinBox(ui->advOutFFVGOPSize, "AdvOut", "FFVGOPSize"); - SaveCheckBox(ui->advOutFFUseRescale, "AdvOut", "FFRescale"); - SaveCheckBox(ui->advOutFFIgnoreCompat, "AdvOut", "FFIgnoreCompat"); - SaveCombo(ui->advOutFFRescale, "AdvOut", "FFRescaleRes"); - SaveEncoder(ui->advOutFFVEncoder, "AdvOut", "FFVEncoder"); - SaveEdit(ui->advOutFFVCfg, "AdvOut", "FFVCustom"); - SaveSpinBox(ui->advOutFFABitrate, "AdvOut", "FFABitrate"); - SaveEncoder(ui->advOutFFAEncoder, "AdvOut", "FFAEncoder"); - SaveEdit(ui->advOutFFACfg, "AdvOut", "FFACustom"); - config_set_int(main->Config(), "AdvOut", "FFAudioMixes", - (ui->advOutFFTrack1->isChecked() ? (1 << 0) : 0) | - (ui->advOutFFTrack2->isChecked() ? (1 << 1) : 0) | - (ui->advOutFFTrack3->isChecked() ? (1 << 2) : 0) | - (ui->advOutFFTrack4->isChecked() ? (1 << 3) : 0) | - (ui->advOutFFTrack5->isChecked() ? (1 << 4) : 0) | - (ui->advOutFFTrack6->isChecked() ? (1 << 5) : 0)); - SaveCombo(ui->advOutTrack1Bitrate, "AdvOut", "Track1Bitrate"); - SaveCombo(ui->advOutTrack2Bitrate, "AdvOut", "Track2Bitrate"); - SaveCombo(ui->advOutTrack3Bitrate, "AdvOut", "Track3Bitrate"); - SaveCombo(ui->advOutTrack4Bitrate, "AdvOut", "Track4Bitrate"); - SaveCombo(ui->advOutTrack5Bitrate, "AdvOut", "Track5Bitrate"); - SaveCombo(ui->advOutTrack6Bitrate, "AdvOut", "Track6Bitrate"); - SaveEdit(ui->advOutTrack1Name, "AdvOut", "Track1Name"); - SaveEdit(ui->advOutTrack2Name, "AdvOut", "Track2Name"); - SaveEdit(ui->advOutTrack3Name, "AdvOut", "Track3Name"); - SaveEdit(ui->advOutTrack4Name, "AdvOut", "Track4Name"); - SaveEdit(ui->advOutTrack5Name, "AdvOut", "Track5Name"); - SaveEdit(ui->advOutTrack6Name, "AdvOut", "Track6Name"); - - if (vodTrackCheckbox) { - SaveCheckBox(simpleVodTrack, "SimpleOutput", "VodTrackEnabled"); - SaveCheckBox(vodTrackCheckbox, "AdvOut", "VodTrackEnabled"); - SaveTrackIndex(main->Config(), "AdvOut", "VodTrackIndex", vodTrack[0], vodTrack[1], vodTrack[2], - vodTrack[3], vodTrack[4], vodTrack[5]); - } - - SaveCheckBox(ui->advReplayBuf, "AdvOut", "RecRB"); - SaveSpinBox(ui->advRBSecMax, "AdvOut", "RecRBTime"); - SaveSpinBox(ui->advRBMegsMax, "AdvOut", "RecRBSize"); - - WriteJsonData(streamEncoderProps, "streamEncoder.json"); - WriteJsonData(recordEncoderProps, "recordEncoder.json"); - main->ResetOutputs(); -} - -void OBSBasicSettings::SaveAudioSettings() -{ - QString sampleRateStr = ui->sampleRate->currentText(); - int channelSetupIdx = ui->channelSetup->currentIndex(); - - const char *channelSetup; - switch (channelSetupIdx) { - case 0: - channelSetup = "Mono"; - break; - case 1: - channelSetup = "Stereo"; - break; - case 2: - channelSetup = "2.1"; - break; - case 3: - channelSetup = "4.0"; - break; - case 4: - channelSetup = "4.1"; - break; - case 5: - channelSetup = "5.1"; - break; - case 6: - channelSetup = "7.1"; - break; - - default: - channelSetup = "Stereo"; - break; - } - - int sampleRate = 44100; - if (sampleRateStr == "48 kHz") - sampleRate = 48000; - - if (WidgetChanged(ui->sampleRate)) - config_set_uint(main->Config(), "Audio", "SampleRate", sampleRate); - - if (WidgetChanged(ui->channelSetup)) - config_set_string(main->Config(), "Audio", "ChannelSetup", channelSetup); - - if (WidgetChanged(ui->meterDecayRate)) { - double meterDecayRate; - switch (ui->meterDecayRate->currentIndex()) { - case 0: - meterDecayRate = VOLUME_METER_DECAY_FAST; - break; - case 1: - meterDecayRate = VOLUME_METER_DECAY_MEDIUM; - break; - case 2: - meterDecayRate = VOLUME_METER_DECAY_SLOW; - break; - default: - meterDecayRate = VOLUME_METER_DECAY_FAST; - break; - } - config_set_double(main->Config(), "Audio", "MeterDecayRate", meterDecayRate); - - main->UpdateVolumeControlsDecayRate(); - } - - if (WidgetChanged(ui->peakMeterType)) { - uint32_t peakMeterTypeIdx = ui->peakMeterType->currentIndex(); - config_set_uint(main->Config(), "Audio", "PeakMeterType", peakMeterTypeIdx); - - main->UpdateVolumeControlsPeakMeterType(); - } - - if (WidgetChanged(ui->lowLatencyBuffering)) { - bool enableLLAudioBuffering = ui->lowLatencyBuffering->isChecked(); - config_set_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering", enableLLAudioBuffering); - } - - for (auto &audioSource : audioSources) { - auto source = OBSGetStrongRef(get<0>(audioSource)); - if (!source) - continue; - - auto &ptmCB = get<1>(audioSource); - auto &ptmSB = get<2>(audioSource); - auto &pttCB = get<3>(audioSource); - auto &pttSB = get<4>(audioSource); - - obs_source_enable_push_to_mute(source, ptmCB->isChecked()); - obs_source_set_push_to_mute_delay(source, ptmSB->value()); - - obs_source_enable_push_to_talk(source, pttCB->isChecked()); - obs_source_set_push_to_talk_delay(source, pttSB->value()); - } - - auto UpdateAudioDevice = [this](bool input, QComboBox *combo, const char *name, int index) { - main->ResetAudioDevice(input ? App()->InputAudioSource() : App()->OutputAudioSource(), - QT_TO_UTF8(GetComboData(combo)), Str(name), index); - }; - - UpdateAudioDevice(false, ui->desktopAudioDevice1, "Basic.DesktopDevice1", 1); - UpdateAudioDevice(false, ui->desktopAudioDevice2, "Basic.DesktopDevice2", 2); - UpdateAudioDevice(true, ui->auxAudioDevice1, "Basic.AuxDevice1", 3); - UpdateAudioDevice(true, ui->auxAudioDevice2, "Basic.AuxDevice2", 4); - UpdateAudioDevice(true, ui->auxAudioDevice3, "Basic.AuxDevice3", 5); - UpdateAudioDevice(true, ui->auxAudioDevice4, "Basic.AuxDevice4", 6); - main->SaveProject(); -} - -void OBSBasicSettings::SaveHotkeySettings() -{ - const auto &config = main->Config(); - - using namespace std; - - std::vector combinations; - for (auto &hotkey : hotkeys) { - auto &hw = *hotkey.second; - if (!hw.Changed()) - continue; - - hw.Save(combinations); - - if (!hotkey.first) - continue; - - OBSDataArrayAutoRelease array = obs_hotkey_save(hw.id); - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "bindings", array); - const char *json = obs_data_get_json(data); - config_set_string(config, "Hotkeys", hw.name.c_str(), json); - } - - if (!main->outputHandler || !main->outputHandler->replayBuffer) - return; - - const char *id = obs_obj_get_id(main->outputHandler->replayBuffer); - if (strcmp(id, "replay_buffer") == 0) { - OBSDataAutoRelease hotkeys = obs_hotkeys_save_output(main->outputHandler->replayBuffer); - config_set_string(config, "Hotkeys", "ReplayBuffer", obs_data_get_json(hotkeys)); - } -} - -#define MINOR_SEPARATOR "------------------------------------------------" - -static void AddChangedVal(std::string &changed, const char *str) -{ - if (changed.size()) - changed += ", "; - changed += str; -} - -void OBSBasicSettings::SaveSettings() -{ - if (generalChanged) - SaveGeneralSettings(); - if (stream1Changed) - SaveStream1Settings(); - if (outputsChanged) - SaveOutputSettings(); - if (audioChanged) - SaveAudioSettings(); - if (videoChanged) - SaveVideoSettings(); - if (hotkeysChanged) - SaveHotkeySettings(); - if (a11yChanged) - SaveA11ySettings(); - if (advancedChanged) - SaveAdvancedSettings(); - if (appearanceChanged) - SaveAppearanceSettings(); - if (videoChanged || advancedChanged) - main->ResetVideo(); - - config_save_safe(main->Config(), "tmp", nullptr); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - main->SaveProject(); - - if (Changed()) { - std::string changed; - if (generalChanged) - AddChangedVal(changed, "general"); - if (stream1Changed) - AddChangedVal(changed, "stream 1"); - if (outputsChanged) - AddChangedVal(changed, "outputs"); - if (audioChanged) - AddChangedVal(changed, "audio"); - if (videoChanged) - AddChangedVal(changed, "video"); - if (hotkeysChanged) - AddChangedVal(changed, "hotkeys"); - if (a11yChanged) - AddChangedVal(changed, "a11y"); - if (appearanceChanged) - AddChangedVal(changed, "appearance"); - if (advancedChanged) - AddChangedVal(changed, "advanced"); - - blog(LOG_INFO, "Settings changed (%s)", changed.c_str()); - blog(LOG_INFO, MINOR_SEPARATOR); - } - - bool langChanged = (ui->language->currentIndex() != prevLangIndex); - bool audioRestart = - (ui->channelSetup->currentIndex() != channelIndex || ui->sampleRate->currentIndex() != sampleRateIndex); - bool browserHWAccelChanged = (ui->browserHWAccel && ui->browserHWAccel->isChecked() != prevBrowserAccel); - - if (langChanged || audioRestart || browserHWAccelChanged) - restart = true; - else - restart = false; -} - -bool OBSBasicSettings::QueryChanges() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr("Basic.Settings.ConfirmTitle"), QTStr("Basic.Settings.Confirm"), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - - if (button == QMessageBox::Cancel) { - return false; - } else if (button == QMessageBox::Yes) { - if (!QueryAllowedToClose()) - return false; - - SaveSettings(); - } else { - if (savedTheme != App()->GetTheme()) - App()->SetTheme(savedTheme->id); - - LoadSettings(true); - restart = false; - } - - ClearChanged(); - return true; -} - -bool OBSBasicSettings::QueryAllowedToClose() -{ - bool simple = (ui->outputMode->currentIndex() == 0); - - bool invalidEncoder = false; - bool invalidFormat = false; - bool invalidTracks = false; - if (simple) { - if (ui->simpleOutRecEncoder->currentIndex() == -1 || ui->simpleOutStrEncoder->currentIndex() == -1 || - ui->simpleOutRecAEncoder->currentIndex() == -1 || ui->simpleOutStrAEncoder->currentIndex() == -1) - invalidEncoder = true; - - if (ui->simpleOutRecFormat->currentIndex() == -1) - invalidFormat = true; - - QString qual = ui->simpleOutRecQuality->currentData().toString(); - QString format = ui->simpleOutRecFormat->currentData().toString(); - if (SimpleOutGetSelectedAudioTracks() == 0 && qual != "Stream" && format != "flv") - invalidTracks = true; - } else { - if (ui->advOutRecEncoder->currentIndex() == -1 || ui->advOutEncoder->currentIndex() == -1 || - ui->advOutRecAEncoder->currentIndex() == -1 || ui->advOutAEncoder->currentIndex() == -1) - invalidEncoder = true; - - QString format = ui->advOutRecFormat->currentData().toString(); - if (AdvOutGetSelectedAudioTracks() == 0 && format != "flv") - invalidTracks = true; - if (AdvOutGetStreamingSelectedAudioTracks() == 0) - invalidTracks = true; - } - - if (invalidEncoder) { - OBSMessageBox::warning(this, QTStr("CodecCompat.CodecMissingOnExit.Title"), - QTStr("CodecCompat.CodecMissingOnExit.Text")); - return false; - } else if (invalidFormat) { - OBSMessageBox::warning(this, QTStr("CodecCompat.ContainerMissingOnExit.Title"), - QTStr("CodecCompat.ContainerMissingOnExit.Text")); - return false; - } else if (invalidTracks) { - OBSMessageBox::warning(this, QTStr("OutputWarnings.NoTracksSelectedOnExit.Title"), - QTStr("OutputWarnings.NoTracksSelectedOnExit.Text")); - return false; - } - - return true; -} - -void OBSBasicSettings::closeEvent(QCloseEvent *event) -{ - if (!AskIfCanCloseSettings()) - event->ignore(); -} - -void OBSBasicSettings::showEvent(QShowEvent *event) -{ - QDialog::showEvent(event); - - /* Reduce the height of the widget area if too tall compared to the screen - * size (e.g., 720p) with potential window decoration (e.g., titlebar). */ - const int titleBarHeight = QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); - const int maxHeight = round(screen()->availableGeometry().height() - titleBarHeight); - if (size().height() >= maxHeight) - resize(size().width(), maxHeight); -} - -void OBSBasicSettings::reject() -{ - if (AskIfCanCloseSettings()) - close(); -} - -void OBSBasicSettings::on_listWidget_itemSelectionChanged() -{ - int row = ui->listWidget->currentRow(); - - if (loading || row == pageIndex) - return; - - if (!hotkeysLoaded && row == Pages::HOTKEYS) { - setCursor(Qt::BusyCursor); - /* Look, I know this /feels/ wrong, but the specific issue we're dealing with - * here means that the UI locks up immediately even when using "invokeMethod". - * So the only way for the user to see the loading message on the page is to - * give the Qt event loop a tiny bit of time to switch to the hotkey page, - * and only then start loading. This could maybe be done by subclassing QWidget - * for the hotkey page and then using showEvent() but I *really* don't want - * to deal with that right now. I've got better things to do with my life - * than to work around this god damn stupid issue for something we'll remove - * soon enough anyway. So this solution it is. */ - QTimer::singleShot(1, this, [&]() { LoadHotkeySettings(); }); - } - - pageIndex = row; -} - -void OBSBasicSettings::UpdateYouTubeAppDockSettings() -{ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - if (cef_js_avail) { - std::string service = ui->service->currentText().toStdString(); - if (IsYouTubeService(service)) { - if (!main->GetYouTubeAppDock()) { - main->NewYouTubeAppDock(); - } - main->GetYouTubeAppDock()->SettingsUpdated(!IsYouTubeService(service) || stream1Changed); - } else { - if (main->GetYouTubeAppDock()) { - main->GetYouTubeAppDock()->AccountDisconnected(); - } - main->DeleteYouTubeAppDock(); - } - } -#endif -} - -void OBSBasicSettings::on_buttonBox_clicked(QAbstractButton *button) -{ - QDialogButtonBox::ButtonRole val = ui->buttonBox->buttonRole(button); - - if (val == QDialogButtonBox::ApplyRole || val == QDialogButtonBox::AcceptRole) { - if (!QueryAllowedToClose()) - return; - - SaveSettings(); - - UpdateYouTubeAppDockSettings(); - ClearChanged(); - } - - if (val == QDialogButtonBox::AcceptRole || val == QDialogButtonBox::RejectRole) { - if (val == QDialogButtonBox::RejectRole) { - if (savedTheme != App()->GetTheme()) - App()->SetTheme(savedTheme->id); - } - ClearChanged(); - close(); - } -} - -void OBSBasicSettings::on_simpleOutputBrowse_clicked() -{ - QString dir = - SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->simpleOutputPath->text()); - if (dir.isEmpty()) - return; - - ui->simpleOutputPath->setText(dir); -} - -void OBSBasicSettings::on_advOutRecPathBrowse_clicked() -{ - QString dir = SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->advOutRecPath->text()); - if (dir.isEmpty()) - return; - - ui->advOutRecPath->setText(dir); -} - -void OBSBasicSettings::on_advOutFFPathBrowse_clicked() -{ - QString dir = SelectDirectory(this, QTStr("Basic.Settings.Output.SelectDirectory"), ui->advOutRecPath->text()); - if (dir.isEmpty()) - return; - - ui->advOutFFRecPath->setText(dir); -} - -void OBSBasicSettings::on_advOutEncoder_currentIndexChanged() -{ - QString encoder = GetComboData(ui->advOutEncoder); - if (!loading) { - bool loadSettings = encoder == curAdvStreamEncoder; - - delete streamEncoderProps; - streamEncoderProps = CreateEncoderPropertyView(QT_TO_UTF8(encoder), - loadSettings ? "streamEncoder.json" : nullptr, true); - streamEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - ui->advOutEncoderLayout->addWidget(streamEncoderProps); - } - - ui->advOutUseRescale->setVisible(true); - ui->advOutRescale->setVisible(true); -} - -void OBSBasicSettings::on_advOutRecEncoder_currentIndexChanged(int idx) -{ - if (!loading) { - delete recordEncoderProps; - recordEncoderProps = nullptr; - } - - if (idx <= 0) { - ui->advOutRecUseRescale->setVisible(false); - ui->advOutRecRescaleContainer->setVisible(false); - ui->advOutRecEncoderProps->setVisible(false); - return; - } - - QString encoder = GetComboData(ui->advOutRecEncoder); - bool loadSettings = encoder == curAdvRecordEncoder; - - if (!loading) { - recordEncoderProps = CreateEncoderPropertyView(QT_TO_UTF8(encoder), - loadSettings ? "recordEncoder.json" : nullptr, true); - recordEncoderProps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - ui->advOutRecEncoderProps->layout()->addWidget(recordEncoderProps); - connect(recordEncoderProps, &OBSPropertiesView::Changed, this, - &OBSBasicSettings::AdvReplayBufferChanged); - } - - ui->advOutRecUseRescale->setVisible(true); - ui->advOutRecRescaleContainer->setVisible(true); - ui->advOutRecEncoderProps->setVisible(true); -} - -void OBSBasicSettings::on_advOutFFIgnoreCompat_stateChanged(int) -{ - /* Little hack to reload codecs when checked */ - on_advOutFFFormat_currentIndexChanged(ui->advOutFFFormat->currentIndex()); -} - -#define DEFAULT_CONTAINER_STR QTStr("Basic.Settings.Output.Adv.FFmpeg.FormatDescDef") - -void OBSBasicSettings::on_advOutFFFormat_currentIndexChanged(int idx) -{ - const QVariant itemDataVariant = ui->advOutFFFormat->itemData(idx); - - if (!itemDataVariant.isNull()) { - auto format = itemDataVariant.value(); - SetAdvOutputFFmpegEnablement(FFmpegCodecType::AUDIO, format.HasAudio(), false); - SetAdvOutputFFmpegEnablement(FFmpegCodecType::VIDEO, format.HasVideo(), false); - ReloadCodecs(format); - - ui->advOutFFFormatDesc->setText(format.long_name); - - FFmpegCodec defaultAudioCodecDesc = format.GetDefaultEncoder(FFmpegCodecType::AUDIO); - FFmpegCodec defaultVideoCodecDesc = format.GetDefaultEncoder(FFmpegCodecType::VIDEO); - SelectEncoder(ui->advOutFFAEncoder, defaultAudioCodecDesc.name, defaultAudioCodecDesc.id); - SelectEncoder(ui->advOutFFVEncoder, defaultVideoCodecDesc.name, defaultVideoCodecDesc.id); - } else { - ui->advOutFFAEncoder->blockSignals(true); - ui->advOutFFVEncoder->blockSignals(true); - ui->advOutFFAEncoder->clear(); - ui->advOutFFVEncoder->clear(); - - ui->advOutFFFormatDesc->setText(DEFAULT_CONTAINER_STR); - } -} - -void OBSBasicSettings::on_advOutFFAEncoder_currentIndexChanged(int idx) -{ - const QVariant itemDataVariant = ui->advOutFFAEncoder->itemData(idx); - if (!itemDataVariant.isNull()) { - auto desc = itemDataVariant.value(); - SetAdvOutputFFmpegEnablement(FFmpegCodecType::AUDIO, desc.id != 0 || desc.name != nullptr, true); - } -} - -void OBSBasicSettings::on_advOutFFVEncoder_currentIndexChanged(int idx) -{ - const QVariant itemDataVariant = ui->advOutFFVEncoder->itemData(idx); - if (!itemDataVariant.isNull()) { - auto desc = itemDataVariant.value(); - SetAdvOutputFFmpegEnablement(FFmpegCodecType::VIDEO, desc.id != 0 || desc.name != nullptr, true); - } -} - -void OBSBasicSettings::on_advOutFFType_currentIndexChanged(int idx) -{ - ui->advOutFFNoSpace->setHidden(idx != 0); -} - -void OBSBasicSettings::on_colorFormat_currentIndexChanged(int) -{ - UpdateColorFormatSpaceWarning(); -} - -void OBSBasicSettings::on_colorSpace_currentIndexChanged(int) -{ - UpdateColorFormatSpaceWarning(); -} - -#define INVALID_RES_STR "Basic.Settings.Video.InvalidResolution" - -static bool ValidResolutions(Ui::OBSBasicSettings *ui) -{ - QString baseRes = ui->baseResolution->lineEdit()->text(); - uint32_t cx, cy; - - if (!ConvertResText(QT_TO_UTF8(baseRes), cx, cy)) { - ui->videoMsg->setText(QTStr(INVALID_RES_STR)); - return false; - } - - bool lockedOutRes = !ui->outputResolution->isEditable(); - if (!lockedOutRes) { - QString outRes = ui->outputResolution->lineEdit()->text(); - if (!ConvertResText(QT_TO_UTF8(outRes), cx, cy)) { - ui->videoMsg->setText(QTStr(INVALID_RES_STR)); - return false; - } - } - - ui->videoMsg->setText(""); - return true; -} - -void OBSBasicSettings::RecalcOutputResPixels(const char *resText) -{ - uint32_t newCX; - uint32_t newCY; - - if (ConvertResText(resText, newCX, newCY) && newCX && newCY) { - outputCX = newCX; - outputCY = newCY; - - std::tuple aspect = aspect_ratio(outputCX, outputCY); - - ui->scaledAspect->setText( - QTStr("AspectRatio") - .arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); - } -} - -bool OBSBasicSettings::AskIfCanCloseSettings() -{ - bool canCloseSettings = false; - - if (!Changed() || QueryChanges()) - canCloseSettings = true; - - if (forceAuthReload) { - main->auth->Save(); - main->auth->Load(); - forceAuthReload = false; - } - - if (forceUpdateCheck) { - main->CheckForUpdates(false); - forceUpdateCheck = false; - } - - return canCloseSettings; -} - -void OBSBasicSettings::on_filenameFormatting_textEdited(const QString &text) -{ - QString safeStr = text; - -#ifdef __APPLE__ - safeStr.replace(QRegularExpression("[:]"), ""); -#elif defined(_WIN32) - safeStr.replace(QRegularExpression("[<>:\"\\|\\?\\*]"), ""); -#else - // TODO: Add filtering for other platforms -#endif - - if (text != safeStr) - ui->filenameFormatting->setText(safeStr); -} - -void OBSBasicSettings::on_outputResolution_editTextChanged(const QString &text) -{ - if (!loading) { - RecalcOutputResPixels(QT_TO_UTF8(text)); - LoadDownscaleFilters(); - } -} - -void OBSBasicSettings::on_baseResolution_editTextChanged(const QString &text) -{ - if (!loading && ValidResolutions(ui.get())) { - QString baseResolution = text; - uint32_t cx, cy; - - ConvertResText(QT_TO_UTF8(baseResolution), cx, cy); - - std::tuple aspect = aspect_ratio(cx, cy); - - ui->baseAspect->setText( - QTStr("AspectRatio") - .arg(QString::number(std::get<0>(aspect)), QString::number(std::get<1>(aspect)))); - - ResetDownscales(cx, cy); - } -} - -void OBSBasicSettings::GeneralChanged() -{ - if (!loading) { - generalChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::Stream1Changed() -{ - if (!loading) { - stream1Changed = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::OutputsChanged() -{ - if (!loading) { - outputsChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - - UpdateMultitrackVideo(); - } -} - -void OBSBasicSettings::AudioChanged() -{ - if (!loading) { - audioChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::AudioChangedRestart() -{ - ui->audioMsg->setVisible(false); - - if (!loading) { - int currentChannelIndex = ui->channelSetup->currentIndex(); - int currentSampleRateIndex = ui->sampleRate->currentIndex(); - bool currentLLAudioBufVal = ui->lowLatencyBuffering->isChecked(); - - if (currentChannelIndex != channelIndex || currentSampleRateIndex != sampleRateIndex || - currentLLAudioBufVal != llBufferingEnabled) { - ui->audioMsg->setText(QTStr("Basic.Settings.ProgramRestart")); - ui->audioMsg->setVisible(true); - } else { - ui->audioMsg->setText(""); - } - - audioChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::ReloadAudioSources() -{ - LoadAudioSources(); -} - -#define MULTI_CHANNEL_WARNING "Basic.Settings.Audio.MultichannelWarning" - -void OBSBasicSettings::SpeakerLayoutChanged(int idx) -{ - QString speakerLayoutQstr = ui->channelSetup->itemText(idx); - std::string speakerLayout = QT_TO_UTF8(speakerLayoutQstr); - bool surround = IsSurround(speakerLayout.c_str()); - bool isOpus = ui->simpleOutStrAEncoder->currentData().toString() == "opus"; - - if (surround) { - /* - * Display all bitrates - */ - PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); - - string stream_encoder_id = ui->advOutAEncoder->currentData().toString().toStdString(); - string record_encoder_id = ui->advOutRecAEncoder->currentData().toString().toStdString(); - PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, - stream_encoder_id.c_str(), - record_encoder_id == "none" ? stream_encoder_id.c_str() - : record_encoder_id.c_str()); - } else { - /* - * Reset audio bitrate for simple and adv mode, update list of - * bitrates and save setting. - */ - RestrictResetBitrates({ui->simpleOutputABitrate, ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, - ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, - ui->advOutTrack6Bitrate}, - 320); - - SaveCombo(ui->simpleOutputABitrate, "SimpleOutput", "ABitrate"); - SaveCombo(ui->advOutTrack1Bitrate, "AdvOut", "Track1Bitrate"); - SaveCombo(ui->advOutTrack2Bitrate, "AdvOut", "Track2Bitrate"); - SaveCombo(ui->advOutTrack3Bitrate, "AdvOut", "Track3Bitrate"); - SaveCombo(ui->advOutTrack4Bitrate, "AdvOut", "Track4Bitrate"); - SaveCombo(ui->advOutTrack5Bitrate, "AdvOut", "Track5Bitrate"); - SaveCombo(ui->advOutTrack6Bitrate, "AdvOut", "Track6Bitrate"); - } - - UpdateAudioWarnings(); -} - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) -void OBSBasicSettings::HideOBSWindowWarning(Qt::CheckState state) -#else -void OBSBasicSettings::HideOBSWindowWarning(int state) -#endif -{ - if (loading || state == Qt::Unchecked) - return; - - if (config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutHideOBSFromCapture")) - return; - - OBSMessageBox::information(this, QTStr("Basic.Settings.General.HideOBSWindowsFromCapture"), - QTStr("Basic.Settings.General.HideOBSWindowsFromCapture.Message")); - - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutHideOBSFromCapture", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); -} - -/* - * resets current bitrate if too large and restricts the number of bitrates - * displayed when multichannel OFF - */ - -void RestrictResetBitrates(initializer_list boxes, int maxbitrate) -{ - for (auto box : boxes) { - int idx = box->currentIndex(); - int max_bitrate = FindClosestAvailableAudioBitrate(box, maxbitrate); - int count = box->count(); - int max_idx = box->findText(QT_UTF8(std::to_string(max_bitrate).c_str())); - - for (int i = (count - 1); i > max_idx; i--) - box->removeItem(i); - - if (idx > max_idx) { - int default_bitrate = FindClosestAvailableAudioBitrate(box, maxbitrate / 2); - int default_idx = box->findText(QT_UTF8(std::to_string(default_bitrate).c_str())); - - box->setCurrentIndex(default_idx); - box->setProperty("changed", QVariant(true)); - } else { - box->setCurrentIndex(idx); - } - } -} - -void OBSBasicSettings::AdvancedChangedRestart() -{ - ui->advancedMsg->setVisible(false); - - if (!loading) { - advancedChanged = true; - ui->advancedMsg->setText(QTStr("Basic.Settings.ProgramRestart")); - ui->advancedMsg->setVisible(true); - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::VideoChangedResolution() -{ - if (!loading && ValidResolutions(ui.get())) { - videoChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::VideoChanged() -{ - if (!loading) { - videoChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::HotkeysChanged() -{ - using namespace std; - if (loading) - return; - - hotkeysChanged = any_of(begin(hotkeys), end(hotkeys), [](const pair> &hotkey) { - const auto &hw = *hotkey.second; - return hw.Changed(); - }); - - if (hotkeysChanged) - EnableApplyButton(true); -} - -void OBSBasicSettings::SearchHotkeys(const QString &text, obs_key_combination_t filterCombo) -{ - - if (ui->hotkeyFormLayout->rowCount() == 0) - return; - - std::vector combos; - bool showHotkey; - ui->hotkeyScrollArea->ensureVisible(0, 0); - - QLayoutItem *hotkeysItem = ui->hotkeyFormLayout->itemAt(0); - QWidget *hotkeys = hotkeysItem->widget(); - if (!hotkeys) - return; - - QFormLayout *hotkeysLayout = qobject_cast(hotkeys->layout()); - hotkeysLayout->setEnabled(false); - - QString needle = text.toLower(); - - for (int i = 0; i < hotkeysLayout->rowCount(); i++) { - auto label = hotkeysLayout->itemAt(i, QFormLayout::LabelRole); - if (!label) - continue; - - OBSHotkeyLabel *item = qobject_cast(label->widget()); - if (!item) - continue; - - QString fullname = item->property("fullName").value(); - - showHotkey = needle.isEmpty() || fullname.toLower().contains(needle); - - if (showHotkey && !obs_key_combination_is_empty(filterCombo)) { - showHotkey = false; - - item->widget->GetCombinations(combos); - for (auto combo : combos) { - if (combo == filterCombo) { - showHotkey = true; - break; - } - } - } - - label->widget()->setVisible(showHotkey); - - auto field = hotkeysLayout->itemAt(i, QFormLayout::FieldRole); - if (field) - field->widget()->setVisible(showHotkey); - } - hotkeysLayout->setEnabled(true); -} - -void OBSBasicSettings::on_hotkeyFilterReset_clicked() -{ - ui->hotkeyFilterSearch->setText(""); - ui->hotkeyFilterInput->ResetKey(); -} - -void OBSBasicSettings::on_hotkeyFilterSearch_textChanged(const QString text) -{ - SearchHotkeys(text, ui->hotkeyFilterInput->key); -} - -void OBSBasicSettings::on_hotkeyFilterInput_KeyChanged(obs_key_combination_t combo) -{ - SearchHotkeys(ui->hotkeyFilterSearch->text(), combo); -} - -namespace std { -template<> struct hash { - size_t operator()(obs_key_combination_t value) const - { - size_t h1 = hash{}(value.modifiers); - size_t h2 = hash{}(value.key); - // Same as boost::hash_combine() - h2 ^= h1 + 0x9e3779b9 + (h2 << 6) + (h2 >> 2); - return h2; - } -}; -} // namespace std - -bool OBSBasicSettings::ScanDuplicateHotkeys(QFormLayout *layout) -{ - typedef struct assignment { - OBSHotkeyLabel *label; - OBSHotkeyEdit *edit; - } assignment; - - unordered_map> assignments; - vector items; - bool hasDupes = false; - - for (int i = 0; i < layout->rowCount(); i++) { - auto label = layout->itemAt(i, QFormLayout::LabelRole); - if (!label) - continue; - OBSHotkeyLabel *item = qobject_cast(label->widget()); - if (!item) - continue; - - items.push_back(item); - - for (auto &edit : item->widget->edits) { - edit->hasDuplicate = false; - - if (obs_key_combination_is_empty(edit->key)) - continue; - - for (assignment &assign : assignments[edit->key]) { - if (item->pairPartner == assign.label) - continue; - - assign.edit->hasDuplicate = true; - edit->hasDuplicate = true; - hasDupes = true; - } - - assignments[edit->key].push_back({item, edit}); - } - } - - for (auto *item : items) - for (auto &edit : item->widget->edits) - edit->UpdateDuplicationState(); - - return hasDupes; -} - -void OBSBasicSettings::ReloadHotkeys(obs_hotkey_id ignoreKey) -{ - if (!hotkeysLoaded) - return; - LoadHotkeySettings(ignoreKey); -} - -void OBSBasicSettings::A11yChanged() -{ - if (!loading) { - a11yChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::AppearanceChanged() -{ - if (!loading) { - appearanceChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::AdvancedChanged() -{ - if (!loading) { - advancedChanged = true; - sender()->setProperty("changed", QVariant(true)); - EnableApplyButton(true); - } -} - -void OBSBasicSettings::AdvOutSplitFileChanged() -{ - bool splitFile = ui->advOutSplitFile->isChecked(); - int splitFileType = splitFile ? ui->advOutSplitFileType->currentIndex() : -1; - - ui->advOutSplitFileType->setEnabled(splitFile); - ui->advOutSplitFileTimeLabel->setVisible(splitFileType == 0); - ui->advOutSplitFileTime->setVisible(splitFileType == 0); - ui->advOutSplitFileSizeLabel->setVisible(splitFileType == 1); - ui->advOutSplitFileSize->setVisible(splitFileType == 1); -} - -static void DisableIncompatibleCodecs(QComboBox *cbox, const QString &format, const QString &formatName, - const QString &streamEncoder) -{ - QString strEncLabel = QTStr("Basic.Settings.Output.Adv.Recording.UseStreamEncoder"); - QString recEncoder = cbox->currentData().toString(); - - /* Check if selected encoders and output format are compatible, disable incompatible items. */ - bool currentCompatible = true; - for (int idx = 0; idx < cbox->count(); idx++) { - QString encName = cbox->itemData(idx).toString(); - string encoderId = (encName == "none") ? streamEncoder.toStdString() : encName.toStdString(); - QString encDisplayName = (encName == "none") ? strEncLabel - : obs_encoder_get_display_name(encoderId.c_str()); - - /* Something has gone horribly wrong and there's no encoder */ - if (encoderId.empty()) - continue; - - if (obs_get_encoder_caps(encoderId.c_str()) & OBS_ENCODER_CAP_DEPRECATED) { - encDisplayName += " (" + QTStr("Deprecated") + ")"; - } - - const char *codec = obs_get_encoder_codec(encoderId.c_str()); - - bool is_compatible = ContainerSupportsCodec(format.toStdString(), codec); - /* Fall back to FFmpeg check if codec not one of the built-in ones. */ - if (!is_compatible && !IsBuiltinCodec(codec)) { - string ext = GetFormatExt(QT_TO_UTF8(format)); - is_compatible = FFCodecAndFormatCompatible(codec, ext.c_str()); - } - - QStandardItemModel *model = dynamic_cast(cbox->model()); - QStandardItem *item = model->item(idx); - - if (is_compatible) { - item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - } else { - if (recEncoder == encName) - currentCompatible = false; - - item->setFlags(Qt::NoItemFlags); - encDisplayName += " "; - encDisplayName += QTStr("CodecCompat.Incompatible").arg(formatName); - } - - item->setText(encDisplayName); - } - - // Set to invalid entry if encoder was incompatible - if (!currentCompatible) - cbox->setCurrentIndex(-1); -} - -void OBSBasicSettings::AdvOutRecCheckCodecs() -{ - QString recFormat = ui->advOutRecFormat->currentData().toString(); - QString recFormatName = ui->advOutRecFormat->currentText(); - - /* Set tooltip if available */ - QString tooltip = QTStr("Basic.Settings.Output.Format.TT." + recFormat.toUtf8()); - - if (!tooltip.startsWith("Basic.Settings.Output")) - ui->advOutRecFormat->setToolTip(tooltip); - else - ui->advOutRecFormat->setToolTip(nullptr); - - QString streamEncoder = ui->advOutEncoder->currentData().toString(); - QString streamAudioEncoder = ui->advOutAEncoder->currentData().toString(); - - int oldVEncoderIdx = ui->advOutRecEncoder->currentIndex(); - int oldAEncoderIdx = ui->advOutRecAEncoder->currentIndex(); - DisableIncompatibleCodecs(ui->advOutRecEncoder, recFormat, recFormatName, streamEncoder); - DisableIncompatibleCodecs(ui->advOutRecAEncoder, recFormat, recFormatName, streamAudioEncoder); - - /* Only invoke AdvOutRecCheckWarnings() if it wouldn't already have - * been triggered by one of the encoder selections being reset. */ - if (ui->advOutRecEncoder->currentIndex() == oldVEncoderIdx && - ui->advOutRecAEncoder->currentIndex() == oldAEncoderIdx) - AdvOutRecCheckWarnings(); -} - -#if defined(__APPLE__) && QT_VERSION < QT_VERSION_CHECK(6, 5, 1) -// Workaround for QTBUG-56064 on macOS -static void ResetInvalidSelection(QComboBox *cbox) -{ - int idx = cbox->currentIndex(); - if (idx < 0) - return; - - QStandardItemModel *model = dynamic_cast(cbox->model()); - QStandardItem *item = model->item(idx); - - if (item->isEnabled()) - return; - - // Reset to "invalid" state if item was disabled - cbox->blockSignals(true); - cbox->setCurrentIndex(-1); - cbox->blockSignals(false); -} -#endif - -void OBSBasicSettings::AdvOutRecCheckWarnings() -{ - auto Checked = [](QCheckBox *box) { - return box->isChecked() ? 1 : 0; - }; - - QString errorMsg; - QString warningMsg; - uint32_t tracks = Checked(ui->advOutRecTrack1) + Checked(ui->advOutRecTrack2) + Checked(ui->advOutRecTrack3) + - Checked(ui->advOutRecTrack4) + Checked(ui->advOutRecTrack5) + Checked(ui->advOutRecTrack6); - - bool useStreamEncoder = ui->advOutRecEncoder->currentIndex() == 0; - if (useStreamEncoder) { - if (!warningMsg.isEmpty()) - warningMsg += "\n\n"; - warningMsg += QTStr("OutputWarnings.CannotPause"); - } - - QString recFormat = ui->advOutRecFormat->currentData().toString(); - - if (recFormat == "flv") { - ui->advRecTrackWidget->setCurrentWidget(ui->flvTracks); - } else { - ui->advRecTrackWidget->setCurrentWidget(ui->recTracks); - - if (tracks == 0) - errorMsg = QTStr("OutputWarnings.NoTracksSelected"); - } - - if (recFormat == "mp4" || recFormat == "mov") { - if (!warningMsg.isEmpty()) - warningMsg += "\n\n"; - - warningMsg += QTStr("OutputWarnings.MP4Recording"); - ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4") + " " + - QTStr("Basic.Settings.Advanced.AutoRemux.MP4")); - } else { - ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4")); - } - -#if defined(__APPLE__) && QT_VERSION < QT_VERSION_CHECK(6, 5, 1) - // Workaround for QTBUG-56064 on macOS - ResetInvalidSelection(ui->advOutRecEncoder); - ResetInvalidSelection(ui->advOutRecAEncoder); -#endif - - // Show warning if codec selection was reset to an invalid state - if (ui->advOutRecEncoder->currentIndex() == -1 || ui->advOutRecAEncoder->currentIndex() == -1) { - if (!warningMsg.isEmpty()) - warningMsg += "\n\n"; - - warningMsg += QTStr("OutputWarnings.CodecIncompatible"); - } - - delete advOutRecWarning; - - if (!errorMsg.isEmpty() || !warningMsg.isEmpty()) { - advOutRecWarning = new QLabel(errorMsg.isEmpty() ? warningMsg : errorMsg, this); - advOutRecWarning->setProperty("class", errorMsg.isEmpty() ? "text-warning" : "text-danger"); - advOutRecWarning->setWordWrap(true); - - ui->advOutRecInfoLayout->addWidget(advOutRecWarning); - } -} - -static inline QString MakeMemorySizeString(int bitrate, int seconds) -{ - QString str = QTStr("Basic.Settings.Advanced.StreamDelay.MemoryUsage"); - int megabytes = bitrate * seconds / 1000 / 8; - - return str.arg(QString::number(megabytes)); -} - -void OBSBasicSettings::UpdateSimpleOutStreamDelayEstimate() -{ - int seconds = ui->streamDelaySec->value(); - int vBitrate = ui->simpleOutputVBitrate->value(); - int aBitrate = ui->simpleOutputABitrate->currentText().toInt(); - - QString msg = MakeMemorySizeString(vBitrate + aBitrate, seconds); - - ui->streamDelayInfo->setText(msg); -} - -void OBSBasicSettings::UpdateAdvOutStreamDelayEstimate() -{ - if (!streamEncoderProps) - return; - - OBSData settings = streamEncoderProps->GetSettings(); - int trackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - QString aBitrateText; - - switch (trackIndex) { - case 1: - aBitrateText = ui->advOutTrack1Bitrate->currentText(); - break; - case 2: - aBitrateText = ui->advOutTrack2Bitrate->currentText(); - break; - case 3: - aBitrateText = ui->advOutTrack3Bitrate->currentText(); - break; - case 4: - aBitrateText = ui->advOutTrack4Bitrate->currentText(); - break; - case 5: - aBitrateText = ui->advOutTrack5Bitrate->currentText(); - break; - case 6: - aBitrateText = ui->advOutTrack6Bitrate->currentText(); - break; - } - - int seconds = ui->streamDelaySec->value(); - int vBitrate = (int)obs_data_get_int(settings, "bitrate"); - int aBitrate = aBitrateText.toInt(); - - QString msg = MakeMemorySizeString(vBitrate + aBitrate, seconds); - - ui->streamDelayInfo->setText(msg); -} - -void OBSBasicSettings::UpdateStreamDelayEstimate() -{ - if (ui->outputMode->currentIndex() == 0) - UpdateSimpleOutStreamDelayEstimate(); - else - UpdateAdvOutStreamDelayEstimate(); - - UpdateAutomaticReplayBufferCheckboxes(); -} - -bool EncoderAvailable(const char *encoder) -{ - const char *val; - int i = 0; - - while (obs_enum_encoder_types(i++, &val)) - if (strcmp(val, encoder) == 0) - return true; - - return false; -} - -void OBSBasicSettings::FillSimpleRecordingValues() -{ -#define ADD_QUALITY(str) \ - ui->simpleOutRecQuality->addItem(QTStr("Basic.Settings.Output.Simple.RecordingQuality." str), QString(str)); -#define ENCODER_STR(str) QTStr("Basic.Settings.Output.Simple.Encoder." str) - - ADD_QUALITY("Stream"); - ADD_QUALITY("Small"); - ADD_QUALITY("HQ"); - ADD_QUALITY("Lossless"); - - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Software"), QString(SIMPLE_ENCODER_X264)); - ui->simpleOutRecEncoder->addItem(ENCODER_STR("SoftwareLowCPU"), QString(SIMPLE_ENCODER_X264_LOWCPU)); - if (EncoderAvailable("obs_qsv11")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.QSV.H264"), QString(SIMPLE_ENCODER_QSV)); - if (EncoderAvailable("obs_qsv11_av1")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.QSV.AV1"), QString(SIMPLE_ENCODER_QSV_AV1)); - if (EncoderAvailable("ffmpeg_nvenc")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.H264"), QString(SIMPLE_ENCODER_NVENC)); - if (EncoderAvailable("obs_nvenc_av1_tex")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.AV1"), QString(SIMPLE_ENCODER_NVENC_AV1)); -#ifdef ENABLE_HEVC - if (EncoderAvailable("h265_texture_amf")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.HEVC"), QString(SIMPLE_ENCODER_AMD_HEVC)); - if (EncoderAvailable("ffmpeg_hevc_nvenc")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.NVENC.HEVC"), - QString(SIMPLE_ENCODER_NVENC_HEVC)); -#endif - if (EncoderAvailable("h264_texture_amf")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.H264"), QString(SIMPLE_ENCODER_AMD)); - if (EncoderAvailable("av1_texture_amf")) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.AMD.AV1"), QString(SIMPLE_ENCODER_AMD_AV1)); - if (EncoderAvailable("com.apple.videotoolbox.videoencoder.ave.avc") -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.Apple.H264"), - QString(SIMPLE_ENCODER_APPLE_H264)); -#ifdef ENABLE_HEVC - if (EncoderAvailable("com.apple.videotoolbox.videoencoder.ave.hevc") -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - ui->simpleOutRecEncoder->addItem(ENCODER_STR("Hardware.Apple.HEVC"), - QString(SIMPLE_ENCODER_APPLE_HEVC)); -#endif - - if (EncoderAvailable("CoreAudio_AAC") || EncoderAvailable("libfdk_aac") || EncoderAvailable("ffmpeg_aac")) - ui->simpleOutRecAEncoder->addItem(QTStr("Basic.Settings.Output.Simple.Codec.AAC.Default"), "aac"); - if (EncoderAvailable("ffmpeg_opus")) - ui->simpleOutRecAEncoder->addItem(QTStr("Basic.Settings.Output.Simple.Codec.Opus"), "opus"); - -#undef ADD_QUALITY -#undef ENCODER_STR -} - -void OBSBasicSettings::FillAudioMonitoringDevices() -{ - QComboBox *cb = ui->monitoringDevice; - - auto enum_devices = [](void *param, const char *name, const char *id) { - QComboBox *cb = (QComboBox *)param; - cb->addItem(name, id); - return true; - }; - - cb->addItem(QTStr("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default"), - "default"); - - obs_enum_audio_monitoring_devices(enum_devices, cb); -} - -void OBSBasicSettings::SimpleRecordingQualityChanged() -{ - QString qual = ui->simpleOutRecQuality->currentData().toString(); - bool streamQuality = qual == "Stream"; - bool losslessQuality = !streamQuality && qual == "Lossless"; - - bool showEncoder = !streamQuality && !losslessQuality; - ui->simpleOutRecEncoder->setVisible(showEncoder); - ui->simpleOutRecEncoderLabel->setVisible(showEncoder); - ui->simpleOutRecAEncoder->setVisible(showEncoder); - ui->simpleOutRecAEncoderLabel->setVisible(showEncoder); - ui->simpleOutRecFormat->setVisible(!losslessQuality); - ui->simpleOutRecFormatLabel->setVisible(!losslessQuality); - - UpdateMultitrackVideo(); - SimpleRecordingEncoderChanged(); - SimpleReplayBufferChanged(); -} - -extern const char *get_simple_output_encoder(const char *encoder); - -void OBSBasicSettings::SimpleStreamingEncoderChanged() -{ - QString encoder = ui->simpleOutStrEncoder->currentData().toString(); - QString preset; - const char *defaultPreset = nullptr; - - ui->simpleOutAdvanced->setVisible(true); - ui->simpleOutPresetLabel->setVisible(true); - ui->simpleOutPreset->setVisible(true); - ui->simpleOutPreset->clear(); - - if (encoder == SIMPLE_ENCODER_QSV || encoder == SIMPLE_ENCODER_QSV_AV1) { - ui->simpleOutPreset->addItem("speed", "speed"); - ui->simpleOutPreset->addItem("balanced", "balanced"); - ui->simpleOutPreset->addItem("quality", "quality"); - - defaultPreset = "balanced"; - preset = curQSVPreset; - - } else if (encoder == SIMPLE_ENCODER_NVENC || encoder == SIMPLE_ENCODER_NVENC_HEVC || - encoder == SIMPLE_ENCODER_NVENC_AV1) { - - const char *name = get_simple_output_encoder(QT_TO_UTF8(encoder)); - const bool isFFmpegEncoder = strncmp(name, "ffmpeg_", 7) == 0; - obs_properties_t *props = obs_get_encoder_properties(name); - - obs_property_t *p = obs_properties_get(props, isFFmpegEncoder ? "preset2" : "preset"); - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *val = obs_property_list_item_string(p, i); - - ui->simpleOutPreset->addItem(QT_UTF8(name), val); - } - - obs_properties_destroy(props); - - defaultPreset = "default"; - preset = curNVENCPreset; - - } else if (encoder == SIMPLE_ENCODER_AMD || encoder == SIMPLE_ENCODER_AMD_HEVC) { - ui->simpleOutPreset->addItem("Speed", "speed"); - ui->simpleOutPreset->addItem("Balanced", "balanced"); - ui->simpleOutPreset->addItem("Quality", "quality"); - - defaultPreset = "balanced"; - preset = curAMDPreset; - } else if (encoder == SIMPLE_ENCODER_APPLE_H264 -#ifdef ENABLE_HEVC - || encoder == SIMPLE_ENCODER_APPLE_HEVC -#endif - ) { - ui->simpleOutAdvanced->setChecked(false); - ui->simpleOutAdvanced->setVisible(false); - ui->simpleOutPreset->setVisible(false); - ui->simpleOutPresetLabel->setVisible(false); - - } else if (encoder == SIMPLE_ENCODER_AMD_AV1) { - ui->simpleOutPreset->addItem("Speed", "speed"); - ui->simpleOutPreset->addItem("Balanced", "balanced"); - ui->simpleOutPreset->addItem("Quality", "quality"); - ui->simpleOutPreset->addItem("High Quality", "highQuality"); - - defaultPreset = "balanced"; - preset = curAMDAV1Preset; - } else { - -#define PRESET_STR(val) QString(Str("Basic.Settings.Output.EncoderPreset." val)).arg(val) - ui->simpleOutPreset->addItem(PRESET_STR("ultrafast"), "ultrafast"); - ui->simpleOutPreset->addItem("superfast", "superfast"); - ui->simpleOutPreset->addItem(PRESET_STR("veryfast"), "veryfast"); - ui->simpleOutPreset->addItem("faster", "faster"); - ui->simpleOutPreset->addItem(PRESET_STR("fast"), "fast"); -#undef PRESET_STR - - /* Users might have previously selected a preset which is no - * longer available in simple mode. Make sure we don't mess - * with their setups without them knowing. */ - if (ui->simpleOutPreset->findData(curPreset) == -1) { - ui->simpleOutPreset->addItem(curPreset, curPreset); - QStandardItemModel *model = qobject_cast(ui->simpleOutPreset->model()); - QStandardItem *item = model->item(model->rowCount() - 1); - item->setEnabled(false); - } - - defaultPreset = "veryfast"; - preset = curPreset; - } - - int idx = ui->simpleOutPreset->findData(QVariant(preset)); - if (idx == -1) - idx = ui->simpleOutPreset->findData(QVariant(defaultPreset)); - - ui->simpleOutPreset->setCurrentIndex(idx); -} - -#define ESTIMATE_STR "Basic.Settings.Output.ReplayBuffer.Estimate" -#define ESTIMATE_TOO_LARGE_STR "Basic.Settings.Output.ReplayBuffer.EstimateTooLarge" -#define ESTIMATE_UNKNOWN_STR "Basic.Settings.Output.ReplayBuffer.EstimateUnknown" - -void OBSBasicSettings::UpdateAutomaticReplayBufferCheckboxes() -{ - bool state = false; - switch (ui->outputMode->currentIndex()) { - case 0: { - const bool lossless = ui->simpleOutRecQuality->currentData().toString() == "Lossless"; - state = ui->simpleReplayBuf->isChecked(); - ui->simpleReplayBuf->setEnabled(!obs_frontend_replay_buffer_active() && !lossless); - break; - } - case 1: { - state = ui->advReplayBuf->isChecked(); - bool customFFmpeg = ui->advOutRecType->currentIndex() == 1; - ui->advReplayBuf->setEnabled(!obs_frontend_replay_buffer_active() && !customFFmpeg); - ui->advReplayBufCustomFFmpeg->setVisible(customFFmpeg); - break; - } - } - ui->replayWhileStreaming->setEnabled(state); - ui->keepReplayStreamStops->setEnabled(state && ui->replayWhileStreaming->isChecked()); -} - -void OBSBasicSettings::SimpleReplayBufferChanged() -{ - QString qual = ui->simpleOutRecQuality->currentData().toString(); - bool streamQuality = qual == "Stream"; - int abitrate = 0; - - ui->simpleRBMegsMax->setVisible(!streamQuality); - ui->simpleRBMegsMaxLabel->setVisible(!streamQuality); - - if (ui->simpleOutRecFormat->currentText().compare("flv") == 0 || streamQuality) { - abitrate = ui->simpleOutputABitrate->currentText().toInt(); - } else { - int delta = ui->simpleOutputABitrate->currentText().toInt(); - if (ui->simpleOutRecTrack1->isChecked()) - abitrate += delta; - if (ui->simpleOutRecTrack2->isChecked()) - abitrate += delta; - if (ui->simpleOutRecTrack3->isChecked()) - abitrate += delta; - if (ui->simpleOutRecTrack4->isChecked()) - abitrate += delta; - if (ui->simpleOutRecTrack5->isChecked()) - abitrate += delta; - if (ui->simpleOutRecTrack6->isChecked()) - abitrate += delta; - } - - int vbitrate = ui->simpleOutputVBitrate->value(); - int seconds = ui->simpleRBSecMax->value(); - - // Set maximum to 75% of installed memory - uint64_t memTotal = os_get_sys_total_size(); - int64_t memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; - - int64_t memMB = int64_t(seconds) * int64_t(vbitrate + abitrate) * 1000 / 8 / 1024 / 1024; - if (memMB < 1) - memMB = 1; - - ui->simpleRBEstimate->setObjectName(""); - if (streamQuality) { - if (memMB <= memMaxMB) { - ui->simpleRBEstimate->setText(QTStr(ESTIMATE_STR).arg(QString::number(int(memMB)))); - } else { - ui->simpleRBEstimate->setText( - QTStr(ESTIMATE_TOO_LARGE_STR) - .arg(QString::number(int(memMB)), QString::number(int(memMaxMB)))); - ui->simpleRBEstimate->setProperty("class", "text-warning"); - } - } else { - ui->simpleRBEstimate->setText(QTStr(ESTIMATE_UNKNOWN_STR)); - ui->simpleRBMegsMax->setMaximum(memMaxMB); - } - - ui->simpleRBEstimate->style()->polish(ui->simpleRBEstimate); - - UpdateAutomaticReplayBufferCheckboxes(); -} - -#define TEXT_USE_STREAM_ENC QTStr("Basic.Settings.Output.Adv.Recording.UseStreamEncoder") - -void OBSBasicSettings::AdvReplayBufferChanged() -{ - obs_data_t *settings; - QString encoder = ui->advOutRecEncoder->currentText(); - bool useStream = QString::compare(encoder, TEXT_USE_STREAM_ENC) == 0; - - if (useStream && streamEncoderProps) { - settings = streamEncoderProps->GetSettings(); - } else if (!useStream && recordEncoderProps) { - settings = recordEncoderProps->GetSettings(); - } else { - if (useStream) - encoder = GetComboData(ui->advOutEncoder); - settings = obs_encoder_defaults(encoder.toUtf8().constData()); - - if (!settings) - return; - - const OBSProfile ¤tProfile = main->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path("recordEncoder.json"); - - if (!jsonFilePath.empty()) { - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - obs_data_apply(settings, data); - } - } - - int vbitrate = (int)obs_data_get_int(settings, "bitrate"); - const char *rateControl = obs_data_get_string(settings, "rate_control"); - - if (!rateControl) - rateControl = ""; - - bool lossless = strcmp(rateControl, "lossless") == 0 || ui->advOutRecType->currentIndex() == 1; - bool replayBufferEnabled = ui->advReplayBuf->isChecked(); - - int abitrate = 0; - if (ui->advOutRecTrack1->isChecked()) - abitrate += ui->advOutTrack1Bitrate->currentText().toInt(); - if (ui->advOutRecTrack2->isChecked()) - abitrate += ui->advOutTrack2Bitrate->currentText().toInt(); - if (ui->advOutRecTrack3->isChecked()) - abitrate += ui->advOutTrack3Bitrate->currentText().toInt(); - if (ui->advOutRecTrack4->isChecked()) - abitrate += ui->advOutTrack4Bitrate->currentText().toInt(); - if (ui->advOutRecTrack5->isChecked()) - abitrate += ui->advOutTrack5Bitrate->currentText().toInt(); - if (ui->advOutRecTrack6->isChecked()) - abitrate += ui->advOutTrack6Bitrate->currentText().toInt(); - - int seconds = ui->advRBSecMax->value(); - - // Set maximum to 75% of installed memory - uint64_t memTotal = os_get_sys_total_size(); - int64_t memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; - - int64_t memMB = int64_t(seconds) * int64_t(vbitrate + abitrate) * 1000 / 8 / 1024 / 1024; - if (memMB < 1) - memMB = 1; - - bool varRateControl = (astrcmpi(rateControl, "CBR") == 0 || astrcmpi(rateControl, "VBR") == 0 || - astrcmpi(rateControl, "ABR") == 0); - if (vbitrate == 0) - varRateControl = false; - - ui->advRBEstimate->setObjectName(""); - if (varRateControl) { - ui->advRBMegsMax->setVisible(false); - ui->advRBMegsMaxLabel->setVisible(false); - - if (memMB <= memMaxMB) { - ui->advRBEstimate->setText(QTStr(ESTIMATE_STR).arg(QString::number(int(memMB)))); - } else { - ui->advRBEstimate->setText( - QTStr(ESTIMATE_TOO_LARGE_STR) - .arg(QString::number(int(memMB)), QString::number(int(memMaxMB)))); - ui->advRBEstimate->setProperty("class", "text-warning"); - } - } else { - ui->advRBMegsMax->setVisible(true); - ui->advRBMegsMaxLabel->setVisible(true); - ui->advRBMegsMax->setMaximum(memMaxMB); - ui->advRBEstimate->setText(QTStr(ESTIMATE_UNKNOWN_STR)); - } - - ui->advReplayBufferFrame->setEnabled(!lossless && replayBufferEnabled); - ui->advRBEstimate->style()->polish(ui->advRBEstimate); - ui->advReplayBuf->setEnabled(!lossless); - - UpdateAutomaticReplayBufferCheckboxes(); -} - -#define SIMPLE_OUTPUT_WARNING(str) QTStr("Basic.Settings.Output.Simple.Warn." str) - -static void DisableIncompatibleSimpleCodecs(QComboBox *cbox, const QString &format) -{ - /* Unlike in advanced mode the available simple mode encoders are - * hardcoded, so this check is also a simpler, hardcoded one. */ - QString encoder = cbox->currentData().toString(); - - bool currentCompatible = true; - for (int idx = 0; idx < cbox->count(); idx++) { - QString encName = cbox->itemData(idx).toString(); - QString codec; - - /* Simple mode does not expose audio encoder variants directly, - * so we have to simply set the codec to the internal name. */ - if (encName == "opus" || encName == "aac") { - codec = encName; - } else { - const char *encoder_id = get_simple_output_encoder(QT_TO_UTF8(encName)); - codec = obs_get_encoder_codec(encoder_id); - } - - QStandardItemModel *model = dynamic_cast(cbox->model()); - QStandardItem *item = model->item(idx); - - if (ContainerSupportsCodec(format.toStdString(), codec.toStdString())) { - item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - } else { - if (encoder == encName) - currentCompatible = false; - - item->setFlags(Qt::NoItemFlags); - } - } - - if (!currentCompatible) - cbox->setCurrentIndex(-1); -} - -static void DisableIncompatibleSimpleContainer(QComboBox *cbox, const QString ¤tFormat, const QString &vEncoder, - const QString &aEncoder) -{ - /* Similar to above, but works in reverse to disable incompatible formats - * based on the encoder selection. */ - string vCodec = obs_get_encoder_codec(get_simple_output_encoder(QT_TO_UTF8(vEncoder))); - string aCodec = aEncoder.toStdString(); - - bool currentCompatible = true; - for (int idx = 0; idx < cbox->count(); idx++) { - QString format = cbox->itemData(idx).toString(); - string formatStr = format.toStdString(); - - QStandardItemModel *model = dynamic_cast(cbox->model()); - QStandardItem *item = model->item(idx); - - if (ContainerSupportsCodec(formatStr, vCodec) && ContainerSupportsCodec(formatStr, aCodec)) { - item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - } else { - if (format == currentFormat) - currentCompatible = false; - - item->setFlags(Qt::NoItemFlags); - } - } - - if (!currentCompatible) - cbox->setCurrentIndex(-1); -} - -void OBSBasicSettings::SimpleRecordingEncoderChanged() -{ - QString qual = ui->simpleOutRecQuality->currentData().toString(); - QString warning; - bool enforceBitrate = !ui->ignoreRecommended->isChecked(); - OBSService service = GetStream1Service(); - - delete simpleOutRecWarning; - - if (enforceBitrate && service) { - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - int oldVBitrate = ui->simpleOutputVBitrate->value(); - int oldABitrate = ui->simpleOutputABitrate->currentText().toInt(); - obs_data_set_int(videoSettings, "bitrate", oldVBitrate); - obs_data_set_int(audioSettings, "bitrate", oldABitrate); - - obs_service_apply_encoder_settings(service, videoSettings, audioSettings); - - int newVBitrate = obs_data_get_int(videoSettings, "bitrate"); - int newABitrate = obs_data_get_int(audioSettings, "bitrate"); - - if (newVBitrate < oldVBitrate) - warning = SIMPLE_OUTPUT_WARNING("VideoBitrate").arg(newVBitrate); - if (newABitrate < oldABitrate) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("AudioBitrate").arg(newABitrate); - } - } - - QString format = ui->simpleOutRecFormat->currentData().toString(); - /* Set tooltip if available */ - QString tooltip = QTStr("Basic.Settings.Output.Format.TT." + format.toUtf8()); - - if (!tooltip.startsWith("Basic.Settings.Output")) - ui->simpleOutRecFormat->setToolTip(tooltip); - else - ui->simpleOutRecFormat->setToolTip(nullptr); - - if (qual == "Lossless") { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("Lossless"); - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("Encoder"); - - } else if (qual != "Stream") { - QString enc = ui->simpleOutRecEncoder->currentData().toString(); - QString streamEnc = ui->simpleOutStrEncoder->currentData().toString(); - bool x264RecEnc = (enc == SIMPLE_ENCODER_X264 || enc == SIMPLE_ENCODER_X264_LOWCPU); - - if (streamEnc == SIMPLE_ENCODER_X264 && x264RecEnc) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("Encoder"); - } - - /* Prevent function being called recursively if changes happen. */ - ui->simpleOutRecEncoder->blockSignals(true); - ui->simpleOutRecAEncoder->blockSignals(true); - DisableIncompatibleSimpleCodecs(ui->simpleOutRecEncoder, format); - DisableIncompatibleSimpleCodecs(ui->simpleOutRecAEncoder, format); - ui->simpleOutRecAEncoder->blockSignals(false); - ui->simpleOutRecEncoder->blockSignals(false); - - if (ui->simpleOutRecEncoder->currentIndex() == -1 || ui->simpleOutRecAEncoder->currentIndex() == -1) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += QTStr("OutputWarnings.CodecIncompatible"); - } - } else { - /* When using stream encoders do the reverse; Disable containers that are incompatible. */ - QString streamEnc = ui->simpleOutStrEncoder->currentData().toString(); - QString streamAEnc = ui->simpleOutStrAEncoder->currentData().toString(); - - ui->simpleOutRecFormat->blockSignals(true); - DisableIncompatibleSimpleContainer(ui->simpleOutRecFormat, format, streamEnc, streamAEnc); - ui->simpleOutRecFormat->blockSignals(false); - - if (ui->simpleOutRecFormat->currentIndex() == -1) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("IncompatibleContainer"); - } - - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("CannotPause"); - } - - if (qual != "Lossless" && (format == "mp4" || format == "mov")) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += QTStr("OutputWarnings.MP4Recording"); - ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4") + " " + - QTStr("Basic.Settings.Advanced.AutoRemux.MP4")); - } else { - ui->autoRemux->setText(QTStr("Basic.Settings.Advanced.AutoRemux").arg("mp4")); - } - - if (qual == "Stream") { - ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleFlvTracks); - } else if (qual == "Lossless") { - ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleRecTracks); - } else { - if (format == "flv") { - ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleFlvTracks); - } else { - ui->simpleRecTrackWidget->setCurrentWidget(ui->simpleRecTracks); - } - } - - if (warning.isEmpty()) - return; - - simpleOutRecWarning = new QLabel(warning, this); - simpleOutRecWarning->setProperty("class", "text-warning"); - simpleOutRecWarning->setWordWrap(true); - ui->simpleOutInfoLayout->addWidget(simpleOutRecWarning); -} - -void OBSBasicSettings::SurroundWarning(int idx) -{ - if (idx == lastChannelSetupIdx || idx == -1) - return; - - if (loading) { - lastChannelSetupIdx = idx; - return; - } - - QString speakerLayoutQstr = ui->channelSetup->itemText(idx); - bool surround = IsSurround(QT_TO_UTF8(speakerLayoutQstr)); - - QString lastQstr = ui->channelSetup->itemText(lastChannelSetupIdx); - bool wasSurround = IsSurround(QT_TO_UTF8(lastQstr)); - - if (surround && !wasSurround) { - QMessageBox::StandardButton button; - - QString warningString = QTStr("Basic.Settings.ProgramRestart") + QStringLiteral("\n\n") + - QTStr(MULTI_CHANNEL_WARNING) + QStringLiteral("\n\n") + - QTStr(MULTI_CHANNEL_WARNING ".Confirm"); - - button = OBSMessageBox::question(this, QTStr(MULTI_CHANNEL_WARNING ".Title"), warningString); - - if (button == QMessageBox::No) { - QMetaObject::invokeMethod(ui->channelSetup, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(int, lastChannelSetupIdx)); - return; - } - } - - lastChannelSetupIdx = idx; -} - -#define LL_BUFFERING_WARNING "Basic.Settings.Audio.LowLatencyBufferingWarning" - -void OBSBasicSettings::UpdateAudioWarnings() -{ - QString speakerLayoutQstr = ui->channelSetup->currentText(); - bool surround = IsSurround(QT_TO_UTF8(speakerLayoutQstr)); - bool lowBufferingActive = ui->lowLatencyBuffering->isChecked(); - - QString text; - - if (surround) { - text = QTStr(MULTI_CHANNEL_WARNING ".Enabled") + QStringLiteral("\n\n") + QTStr(MULTI_CHANNEL_WARNING); - } - - if (lowBufferingActive) { - if (!text.isEmpty()) - text += QStringLiteral("\n\n"); - - text += QTStr(LL_BUFFERING_WARNING ".Enabled") + QStringLiteral("\n\n") + QTStr(LL_BUFFERING_WARNING); - } - - ui->audioMsg_2->setText(text); - ui->audioMsg_2->setVisible(!text.isEmpty()); -} - -void OBSBasicSettings::LowLatencyBufferingChanged(bool checked) -{ - if (checked) { - QString warningStr = - QTStr(LL_BUFFERING_WARNING) + QStringLiteral("\n\n") + QTStr(LL_BUFFERING_WARNING ".Confirm"); - - auto button = OBSMessageBox::question(this, QTStr(LL_BUFFERING_WARNING ".Title"), warningStr); - - if (button == QMessageBox::No) { - QMetaObject::invokeMethod(ui->lowLatencyBuffering, "setChecked", Qt::QueuedConnection, - Q_ARG(bool, false)); - return; - } - } - - QMetaObject::invokeMethod(this, "UpdateAudioWarnings", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "AudioChangedRestart"); -} - -void OBSBasicSettings::SimpleRecordingQualityLosslessWarning(int idx) -{ - if (idx == lastSimpleRecQualityIdx || idx == -1) - return; - - QString qual = ui->simpleOutRecQuality->itemData(idx).toString(); - - if (loading) { - lastSimpleRecQualityIdx = idx; - return; - } - - if (qual == "Lossless") { - QMessageBox::StandardButton button; - - QString warningString = - SIMPLE_OUTPUT_WARNING("Lossless") + QString("\n\n") + SIMPLE_OUTPUT_WARNING("Lossless.Msg"); - - button = OBSMessageBox::question(this, SIMPLE_OUTPUT_WARNING("Lossless.Title"), warningString); - - if (button == QMessageBox::No) { - QMetaObject::invokeMethod(ui->simpleOutRecQuality, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(int, lastSimpleRecQualityIdx)); - return; - } - } - - lastSimpleRecQualityIdx = idx; -} - -void OBSBasicSettings::on_disableOSXVSync_clicked() -{ -#ifdef __APPLE__ - if (!loading) { - bool disable = ui->disableOSXVSync->isChecked(); - ui->resetOSXVSync->setEnabled(disable); - } -#endif -} - -QIcon OBSBasicSettings::GetGeneralIcon() const -{ - return generalIcon; -} - -QIcon OBSBasicSettings::GetAppearanceIcon() const -{ - return appearanceIcon; -} - -QIcon OBSBasicSettings::GetStreamIcon() const -{ - return streamIcon; -} - -QIcon OBSBasicSettings::GetOutputIcon() const -{ - return outputIcon; -} - -QIcon OBSBasicSettings::GetAudioIcon() const -{ - return audioIcon; -} - -QIcon OBSBasicSettings::GetVideoIcon() const -{ - return videoIcon; -} - -QIcon OBSBasicSettings::GetHotkeysIcon() const -{ - return hotkeysIcon; -} - -QIcon OBSBasicSettings::GetAccessibilityIcon() const -{ - return accessibilityIcon; -} - -QIcon OBSBasicSettings::GetAdvancedIcon() const -{ - return advancedIcon; -} - -void OBSBasicSettings::SetGeneralIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::GENERAL)->setIcon(icon); -} - -void OBSBasicSettings::SetAppearanceIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::APPEARANCE)->setIcon(icon); -} - -void OBSBasicSettings::SetStreamIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::STREAM)->setIcon(icon); -} - -void OBSBasicSettings::SetOutputIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::OUTPUT)->setIcon(icon); -} - -void OBSBasicSettings::SetAudioIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::AUDIO)->setIcon(icon); -} - -void OBSBasicSettings::SetVideoIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::VIDEO)->setIcon(icon); -} - -void OBSBasicSettings::SetHotkeysIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::HOTKEYS)->setIcon(icon); -} - -void OBSBasicSettings::SetAccessibilityIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::ACCESSIBILITY)->setIcon(icon); -} - -void OBSBasicSettings::SetAdvancedIcon(const QIcon &icon) -{ - ui->listWidget->item(Pages::ADVANCED)->setIcon(icon); -} - -int OBSBasicSettings::CurrentFLVTrack() -{ - if (ui->flvTrack1->isChecked()) - return 1; - else if (ui->flvTrack2->isChecked()) - return 2; - else if (ui->flvTrack3->isChecked()) - return 3; - else if (ui->flvTrack4->isChecked()) - return 4; - else if (ui->flvTrack5->isChecked()) - return 5; - else if (ui->flvTrack6->isChecked()) - return 6; - - return 0; -} - -int OBSBasicSettings::SimpleOutGetSelectedAudioTracks() -{ - int tracks = (ui->simpleOutRecTrack1->isChecked() ? (1 << 0) : 0) | - (ui->simpleOutRecTrack2->isChecked() ? (1 << 1) : 0) | - (ui->simpleOutRecTrack3->isChecked() ? (1 << 2) : 0) | - (ui->simpleOutRecTrack4->isChecked() ? (1 << 3) : 0) | - (ui->simpleOutRecTrack5->isChecked() ? (1 << 4) : 0) | - (ui->simpleOutRecTrack6->isChecked() ? (1 << 5) : 0); - return tracks; -} - -int OBSBasicSettings::AdvOutGetSelectedAudioTracks() -{ - int tracks = - (ui->advOutRecTrack1->isChecked() ? (1 << 0) : 0) | (ui->advOutRecTrack2->isChecked() ? (1 << 1) : 0) | - (ui->advOutRecTrack3->isChecked() ? (1 << 2) : 0) | (ui->advOutRecTrack4->isChecked() ? (1 << 3) : 0) | - (ui->advOutRecTrack5->isChecked() ? (1 << 4) : 0) | (ui->advOutRecTrack6->isChecked() ? (1 << 5) : 0); - return tracks; -} - -int OBSBasicSettings::AdvOutGetStreamingSelectedAudioTracks() -{ - int tracks = (ui->advOutMultiTrack1->isChecked() ? (1 << 0) : 0) | - (ui->advOutMultiTrack2->isChecked() ? (1 << 1) : 0) | - (ui->advOutMultiTrack3->isChecked() ? (1 << 2) : 0) | - (ui->advOutMultiTrack4->isChecked() ? (1 << 3) : 0) | - (ui->advOutMultiTrack5->isChecked() ? (1 << 4) : 0) | - (ui->advOutMultiTrack6->isChecked() ? (1 << 5) : 0); - return tracks; -} - -/* Using setEditable(true) on a QComboBox when there's a custom style in use - * does not work properly, so instead completely recreate the widget, which - * seems to work fine. */ -void OBSBasicSettings::RecreateOutputResolutionWidget() -{ - QSizePolicy sizePolicy = ui->outputResolution->sizePolicy(); - bool changed = WidgetChanged(ui->outputResolution); - - delete ui->outputResolution; - ui->outputResolution = new QComboBox(ui->videoPage); - ui->outputResolution->setObjectName(QString::fromUtf8("outputResolution")); - ui->outputResolution->setSizePolicy(sizePolicy); - ui->outputResolution->setEditable(true); - ui->outputResolution->setProperty("changed", changed); - ui->outputResLabel->setBuddy(ui->outputResolution); - - ui->outputResLayout->insertWidget(0, ui->outputResolution); - - QWidget::setTabOrder(ui->baseResolution, ui->outputResolution); - QWidget::setTabOrder(ui->outputResolution, ui->downscaleFilter); - - HookWidget(ui->outputResolution, CBEDIT_CHANGED, VIDEO_RES); - - connect(ui->outputResolution, &QComboBox::editTextChanged, this, - &OBSBasicSettings::on_outputResolution_editTextChanged); - - ui->outputResolution->lineEdit()->setValidator(ui->baseResolution->lineEdit()->validator()); -} - -void OBSBasicSettings::UpdateAdvNetworkGroup() -{ - bool enabled = protocol.contains("RTMP"); - - ui->advNetworkDisabled->setVisible(!enabled); - - ui->bindToIPLabel->setVisible(enabled); - ui->bindToIP->setVisible(enabled); - ui->dynBitrate->setVisible(enabled); - ui->ipFamilyLabel->setVisible(enabled); - ui->ipFamily->setVisible(enabled); -#ifdef _WIN32 - ui->enableNewSocketLoop->setVisible(enabled); - ui->enableLowLatencyMode->setVisible(enabled); -#endif -} - -extern bool MultitrackVideoDeveloperModeEnabled(); - -void OBSBasicSettings::UpdateMultitrackVideo() -{ - // Technically, it should currently be safe to toggle multitrackVideo - // while not streaming (recording should be irrelevant), but practically - // output settings aren't currently being tracked with that degree of - // flexibility, so just disable everything while outputs are active. - auto toggle_available = !main->Active(); - - // FIXME: protocol is not updated properly for WHIP; what do? - auto available = protocol.startsWith("RTMP"); - - if (available && !IsCustomService()) { - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); - OBSServiceAutoRelease temp_service = - obs_service_create_private("rtmp_common", "auto config query service", settings); - settings = obs_service_get_settings(temp_service); - available = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - if (!available && ui->enableMultitrackVideo->isChecked()) - ui->enableMultitrackVideo->setChecked(false); - } - -#ifndef _WIN32 - available = available && MultitrackVideoDeveloperModeEnabled(); -#endif - - if (IsCustomService()) - available = available && MultitrackVideoDeveloperModeEnabled(); - - ui->multitrackVideoGroupBox->setVisible(available); - - ui->enableMultitrackVideo->setEnabled(toggle_available); - - ui->multitrackVideoMaximumAggregateBitrateLabel->setEnabled(toggle_available && - ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoMaximumAggregateBitrateAuto->setEnabled(toggle_available && - ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoMaximumAggregateBitrate->setEnabled( - toggle_available && ui->enableMultitrackVideo->isChecked() && - !ui->multitrackVideoMaximumAggregateBitrateAuto->isChecked()); - - ui->multitrackVideoMaximumVideoTracksLabel->setEnabled(toggle_available && - ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoMaximumVideoTracksAuto->setEnabled(toggle_available && - ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoMaximumVideoTracks->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && - !ui->multitrackVideoMaximumVideoTracksAuto->isChecked()); - - ui->multitrackVideoStreamDumpEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled()); - ui->multitrackVideoConfigOverrideEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled()); - ui->multitrackVideoConfigOverrideLabel->setVisible(available && MultitrackVideoDeveloperModeEnabled()); - ui->multitrackVideoConfigOverride->setVisible(available && MultitrackVideoDeveloperModeEnabled()); - - ui->multitrackVideoStreamDumpEnable->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoConfigOverrideEnable->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked()); - ui->multitrackVideoConfigOverrideLabel->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && - ui->multitrackVideoConfigOverrideEnable->isChecked()); - ui->multitrackVideoConfigOverride->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() && - ui->multitrackVideoConfigOverrideEnable->isChecked()); - - auto update_simple_output_settings = [&](bool mtv_enabled) { - auto recording_uses_stream_encoder = ui->simpleOutRecQuality->currentData().toString() == "Stream"; - mtv_enabled = mtv_enabled && !recording_uses_stream_encoder; - - ui->simpleOutputVBitrateLabel->setDisabled(mtv_enabled); - ui->simpleOutputVBitrate->setDisabled(mtv_enabled); - - ui->simpleOutputABitrateLabel->setDisabled(mtv_enabled); - ui->simpleOutputABitrate->setDisabled(mtv_enabled); - - ui->simpleOutStrEncoderLabel->setDisabled(mtv_enabled); - ui->simpleOutStrEncoder->setDisabled(mtv_enabled); - - ui->simpleOutPresetLabel->setDisabled(mtv_enabled); - ui->simpleOutPreset->setDisabled(mtv_enabled); - - ui->simpleOutCustomLabel->setDisabled(mtv_enabled); - ui->simpleOutCustom->setDisabled(mtv_enabled); - - ui->simpleOutStrAEncoderLabel->setDisabled(mtv_enabled); - ui->simpleOutStrAEncoder->setDisabled(mtv_enabled); - }; - - auto update_advanced_output_settings = [&](bool mtv_enabled) { - auto recording_uses_stream_video_encoder = ui->advOutRecEncoder->currentText() == TEXT_USE_STREAM_ENC; - auto recording_uses_stream_audio_encoder = ui->advOutRecAEncoder->currentData() == "none"; - auto disable_video = mtv_enabled && !recording_uses_stream_video_encoder; - auto disable_audio = mtv_enabled && !recording_uses_stream_audio_encoder; - - ui->advOutAEncLabel->setDisabled(disable_audio); - ui->advOutAEncoder->setDisabled(disable_audio); - - ui->advOutEncLabel->setDisabled(disable_video); - ui->advOutEncoder->setDisabled(disable_video); - - ui->advOutUseRescale->setDisabled(disable_video); - ui->advOutRescale->setDisabled(disable_video); - ui->advOutRescaleFilter->setDisabled(disable_video); - - if (streamEncoderProps) - streamEncoderProps->SetDisabled(disable_video); - }; - - auto update_advanced_output_audio_tracks = [&](bool mtv_enabled) { - auto vod_track_enabled = vodTrackCheckbox && vodTrackCheckbox->isChecked(); - - auto vod_track_idx_enabled = [&](size_t idx) { - return vod_track_enabled && vodTrack[idx - 1] && vodTrack[idx - 1]->isChecked(); - }; - - auto track1_warning_visible = mtv_enabled && - (ui->advOutTrack1->isChecked() || vod_track_idx_enabled(1)); - auto track1_disabled = track1_warning_visible && !ui->advOutRecTrack1->isChecked(); - ui->advOutTrack1BitrateLabel->setDisabled(track1_disabled); - ui->advOutTrack1Bitrate->setDisabled(track1_disabled); - - auto track2_warning_visible = mtv_enabled && - (ui->advOutTrack2->isChecked() || vod_track_idx_enabled(2)); - auto track2_disabled = track2_warning_visible && !ui->advOutRecTrack2->isChecked(); - ui->advOutTrack2BitrateLabel->setDisabled(track2_disabled); - ui->advOutTrack2Bitrate->setDisabled(track2_disabled); - - auto track3_warning_visible = mtv_enabled && - (ui->advOutTrack3->isChecked() || vod_track_idx_enabled(3)); - auto track3_disabled = track3_warning_visible && !ui->advOutRecTrack3->isChecked(); - ui->advOutTrack3BitrateLabel->setDisabled(track3_disabled); - ui->advOutTrack3Bitrate->setDisabled(track3_disabled); - - auto track4_warning_visible = mtv_enabled && - (ui->advOutTrack4->isChecked() || vod_track_idx_enabled(4)); - auto track4_disabled = track4_warning_visible && !ui->advOutRecTrack4->isChecked(); - ui->advOutTrack4BitrateLabel->setDisabled(track4_disabled); - ui->advOutTrack4Bitrate->setDisabled(track4_disabled); - - auto track5_warning_visible = mtv_enabled && - (ui->advOutTrack5->isChecked() || vod_track_idx_enabled(5)); - auto track5_disabled = track5_warning_visible && !ui->advOutRecTrack5->isChecked(); - ui->advOutTrack5BitrateLabel->setDisabled(track5_disabled); - ui->advOutTrack5Bitrate->setDisabled(track5_disabled); - - auto track6_warning_visible = mtv_enabled && - (ui->advOutTrack6->isChecked() || vod_track_idx_enabled(6)); - auto track6_disabled = track6_warning_visible && !ui->advOutRecTrack6->isChecked(); - ui->advOutTrack6BitrateLabel->setDisabled(track6_disabled); - ui->advOutTrack6Bitrate->setDisabled(track6_disabled); - }; - - if (available) { - OBSDataAutoRelease settings; - { - auto service_name = ui->service->currentText(); - auto custom_server = ui->customServer->text().trimmed(); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *service = obs_properties_get(props, "service"); - - settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(service_name)); - obs_property_modified(service, settings); - - obs_properties_destroy(props); - } - - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(settings, "multitrack_video_name")) - multitrack_video_name = obs_data_get_string(settings, "multitrack_video_name"); - - ui->enableMultitrackVideo->setText( - QTStr("Basic.Settings.Stream.EnableMultitrackVideo").arg(multitrack_video_name)); - - if (obs_data_has_user_value(settings, "multitrack_video_disclaimer")) { - ui->multitrackVideoInfo->setVisible(true); - ui->multitrackVideoInfo->setText(obs_data_get_string(settings, "multitrack_video_disclaimer")); - } else { - ui->multitrackVideoInfo->setText( - QTStr("MultitrackVideo.Info").arg(multitrack_video_name, ui->service->currentText())); - } - - auto disabled_text = QTStr("Basic.Settings.MultitrackVideoDisabledSettings") - .arg(ui->service->currentText()) - .arg(multitrack_video_name); - - ui->multitrackVideoNotice->setText(disabled_text); - - auto mtv_enabled = ui->enableMultitrackVideo->isChecked(); - ui->multitrackVideoNoticeBox->setVisible(mtv_enabled); - - update_simple_output_settings(mtv_enabled); - update_advanced_output_settings(mtv_enabled); - update_advanced_output_audio_tracks(mtv_enabled); - } else { - ui->multitrackVideoNoticeBox->setVisible(false); - - update_simple_output_settings(false); - update_advanced_output_settings(false); - update_advanced_output_audio_tracks(false); - } -} - -void OBSBasicSettings::SimpleStreamAudioEncoderChanged() -{ - PopulateSimpleBitrates(ui->simpleOutputABitrate, ui->simpleOutStrAEncoder->currentData().toString() == "opus"); - - if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) - return; - - RestrictResetBitrates({ui->simpleOutputABitrate}, 320); -} - -void OBSBasicSettings::AdvAudioEncodersChanged() -{ - QString streamEncoder = ui->advOutAEncoder->currentData().toString(); - QString recEncoder = ui->advOutRecAEncoder->currentData().toString(); - - if (recEncoder == "none") - recEncoder = streamEncoder; - - PopulateAdvancedBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, - QT_TO_UTF8(streamEncoder), QT_TO_UTF8(recEncoder)); - - if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) - return; - - RestrictResetBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, - 320); -} From 664a719421db63825aead425ced15f5665708f28 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 03:30:54 +0100 Subject: [PATCH 17/37] frontend: Prepare sources for merge with OBSBasic modules --- .../widgets/OBSBasic_Browser.cpp | 0 .../widgets/OBSBasic_SceneCollections.cpp | 0 .../widgets/OBSBasic_StudioMode.cpp | 0 frontend/widgets/OBSBasic_Transitions.cpp | 1699 +++++++++++++++++ 4 files changed, 1699 insertions(+) rename UI/window-extra-browsers.cpp => frontend/widgets/OBSBasic_Browser.cpp (100%) rename UI/window-basic-main-scene-collections.cpp => frontend/widgets/OBSBasic_SceneCollections.cpp (100%) rename UI/window-basic-main-transitions.cpp => frontend/widgets/OBSBasic_StudioMode.cpp (100%) create mode 100644 frontend/widgets/OBSBasic_Transitions.cpp diff --git a/UI/window-extra-browsers.cpp b/frontend/widgets/OBSBasic_Browser.cpp similarity index 100% rename from UI/window-extra-browsers.cpp rename to frontend/widgets/OBSBasic_Browser.cpp diff --git a/UI/window-basic-main-scene-collections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp similarity index 100% rename from UI/window-basic-main-scene-collections.cpp rename to frontend/widgets/OBSBasic_SceneCollections.cpp diff --git a/UI/window-basic-main-transitions.cpp b/frontend/widgets/OBSBasic_StudioMode.cpp similarity index 100% rename from UI/window-basic-main-transitions.cpp rename to frontend/widgets/OBSBasic_StudioMode.cpp diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp new file mode 100644 index 000000000..a17137b2b --- /dev/null +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -0,0 +1,1699 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "display-helpers.hpp" +#include "window-namedialog.hpp" +#include "menu-button.hpp" + +#include "obs-hotkey.h" + +using namespace std; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(QuickTransition); + +static inline QString MakeQuickTransitionText(QuickTransition *qt) +{ + QString name; + + if (!qt->fadeToBlack) + name = QT_UTF8(obs_source_get_name(qt->source)); + else + name = QTStr("FadeToBlack"); + + if (!obs_transition_fixed(qt->source)) + name += QString(" (%1ms)").arg(QString::number(qt->duration)); + return name; +} + +void OBSBasic::InitDefaultTransitions() +{ + std::vector transitions; + size_t idx = 0; + const char *id; + + /* automatically add transitions that have no configuration (things + * such as cut/fade/etc) */ + while (obs_enum_transition_types(idx++, &id)) { + if (!obs_is_source_configurable(id)) { + const char *name = obs_source_get_display_name(id); + + OBSSourceAutoRelease tr = obs_source_create_private(id, name, NULL); + InitTransition(tr); + transitions.emplace_back(tr); + + if (strcmp(id, "fade_transition") == 0) + fadeTransition = tr; + else if (strcmp(id, "cut_transition") == 0) + cutTransition = tr; + } + } + + for (OBSSource &tr : transitions) { + ui->transitions->addItem(QT_UTF8(obs_source_get_name(tr)), QVariant::fromValue(OBSSource(tr))); + } +} + +void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) +{ + DStr hotkeyId; + QString hotkeyName; + + dstr_printf(hotkeyId, "OBSBasic.QuickTransition.%d", qt->id); + hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); + + auto quickTransition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + int id = (int)(uintptr_t)data; + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (pressed) + QMetaObject::invokeMethod(main, "TriggerQuickTransition", Qt::QueuedConnection, Q_ARG(int, id)); + }; + + qt->hotkey = obs_hotkey_register_frontend(hotkeyId->array, QT_TO_UTF8(hotkeyName), quickTransition, + (void *)(uintptr_t)qt->id); +} + +void QuickTransition::SourceRenamed(void *param, calldata_t *) +{ + QuickTransition *qt = reinterpret_cast(param); + + QString hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); + + obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); +} + +void OBSBasic::TriggerQuickTransition(int id) +{ + QuickTransition *qt = GetQuickTransition(id); + + if (qt && previewProgramMode) { + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (GetCurrentTransition() != qt->source) { + OverrideTransition(qt->source); + overridingTransition = true; + } + + TransitionToScene(source, false, true, qt->duration, qt->fadeToBlack); + } +} + +void OBSBasic::RemoveQuickTransitionHotkey(QuickTransition *qt) +{ + obs_hotkey_unregister(qt->hotkey); +} + +void OBSBasic::InitTransition(obs_source_t *transition) +{ + auto onTransitionStop = [](void *data, calldata_t *) { + OBSBasic *window = (OBSBasic *)data; + QMetaObject::invokeMethod(window, "TransitionStopped", Qt::QueuedConnection); + }; + + auto onTransitionFullStop = [](void *data, calldata_t *) { + OBSBasic *window = (OBSBasic *)data; + QMetaObject::invokeMethod(window, "TransitionFullyStopped", Qt::QueuedConnection); + }; + + signal_handler_t *handler = obs_source_get_signal_handler(transition); + signal_handler_connect(handler, "transition_video_stop", onTransitionStop, this); + signal_handler_connect(handler, "transition_stop", onTransitionFullStop, this); +} + +static inline OBSSource GetTransitionComboItem(QComboBox *combo, int idx) +{ + return combo->itemData(idx).value(); +} + +void OBSBasic::CreateDefaultQuickTransitions() +{ + /* non-configurable transitions are always available, so add them + * to the "default quick transitions" list */ + quickTransitions.emplace_back(cutTransition, 300, quickTransitionIdCounter++); + quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++); + quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++, true); +} + +void OBSBasic::LoadQuickTransitions(obs_data_array_t *array) +{ + size_t count = obs_data_array_count(array); + + quickTransitionIdCounter = 1; + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + OBSDataArrayAutoRelease hotkeys = obs_data_get_array(data, "hotkeys"); + const char *name = obs_data_get_string(data, "name"); + int duration = obs_data_get_int(data, "duration"); + int id = obs_data_get_int(data, "id"); + bool toBlack = obs_data_get_bool(data, "fade_to_black"); + + if (id) { + obs_source_t *source = FindTransition(name); + if (source) { + quickTransitions.emplace_back(source, duration, id, toBlack); + + if (quickTransitionIdCounter <= id) + quickTransitionIdCounter = id + 1; + + int idx = (int)quickTransitions.size() - 1; + AddQuickTransitionHotkey(&quickTransitions[idx]); + obs_hotkey_load(quickTransitions[idx].hotkey, hotkeys); + } + } + } +} + +obs_data_array_t *OBSBasic::SaveQuickTransitions() +{ + obs_data_array_t *array = obs_data_array_create(); + + for (QuickTransition &qt : quickTransitions) { + OBSDataAutoRelease data = obs_data_create(); + OBSDataArrayAutoRelease hotkeys = obs_hotkey_save(qt.hotkey); + + obs_data_set_string(data, "name", obs_source_get_name(qt.source)); + obs_data_set_int(data, "duration", qt.duration); + obs_data_set_array(data, "hotkeys", hotkeys); + obs_data_set_int(data, "id", qt.id); + obs_data_set_bool(data, "fade_to_black", qt.fadeToBlack); + + obs_data_array_push_back(array, data); + } + + return array; +} + +obs_source_t *OBSBasic::FindTransition(const char *name) +{ + for (int i = 0; i < ui->transitions->count(); i++) { + OBSSource tr = ui->transitions->itemData(i).value(); + if (!tr) + continue; + + const char *trName = obs_source_get_name(tr); + if (strcmp(trName, name) == 0) + return tr; + } + + return nullptr; +} + +void OBSBasic::TransitionToScene(OBSScene scene, bool force) +{ + obs_source_t *source = obs_scene_get_source(scene); + TransitionToScene(source, force); +} + +void OBSBasic::TransitionStopped() +{ + if (swapScenesMode) { + OBSSource scene = OBSGetStrongRef(swapScene); + if (scene) + SetCurrentScene(scene); + } + + EnableTransitionWidgets(true); + UpdatePreviewProgramIndicators(); + + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_STOPPED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + + swapScene = nullptr; +} + +void OBSBasic::OverrideTransition(OBSSource transition) +{ + OBSSourceAutoRelease oldTransition = obs_get_output_source(0); + + if (transition != oldTransition) { + obs_transition_swap_begin(transition, oldTransition); + obs_set_output_source(0, transition); + obs_transition_swap_end(transition, oldTransition); + } +} + +void OBSBasic::TransitionFullyStopped() +{ + if (overridingTransition) { + OverrideTransition(GetCurrentTransition()); + overridingTransition = false; + } +} + +void OBSBasic::TransitionToScene(OBSSource source, bool force, bool quickTransition, int quickDuration, bool black, + bool manual) +{ + obs_scene_t *scene = obs_scene_from_source(source); + bool usingPreviewProgram = IsPreviewProgramMode(); + if (!scene) + return; + + if (usingPreviewProgram) { + if (!tBarActive) + lastProgramScene = programScene; + programScene = OBSGetWeakRef(source); + + if (!force && !black) { + OBSSource lastScene = OBSGetStrongRef(lastProgramScene); + + if (!sceneDuplicationMode && lastScene == source) + return; + + if (swapScenesMode && lastScene && lastScene != GetCurrentSceneSource()) + swapScene = lastProgramScene; + } + } + + if (usingPreviewProgram && sceneDuplicationMode) { + scene = obs_scene_duplicate(scene, obs_source_get_name(obs_scene_get_source(scene)), + editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY + : OBS_SCENE_DUP_PRIVATE_REFS); + source = obs_scene_get_source(scene); + } + + OBSSourceAutoRelease transition = obs_get_output_source(0); + if (!transition) { + if (usingPreviewProgram && sceneDuplicationMode) + obs_scene_release(scene); + return; + } + + float t = obs_transition_get_time(transition); + bool stillTransitioning = t < 1.0f && t > 0.0f; + + // If actively transitioning, block new transitions from starting + if (usingPreviewProgram && stillTransitioning) + goto cleanup; + + if (usingPreviewProgram) { + if (!black && !manual) { + const char *sceneName = obs_source_get_name(source); + blog(LOG_INFO, "User switched Program to scene '%s'", sceneName); + + } else if (black && !prevFTBSource) { + OBSSourceAutoRelease target = obs_transition_get_active_source(transition); + const char *sceneName = obs_source_get_name(target); + blog(LOG_INFO, "User faded from scene '%s' to black", sceneName); + + } else if (black && prevFTBSource) { + const char *sceneName = obs_source_get_name(prevFTBSource); + blog(LOG_INFO, "User faded from black to scene '%s'", sceneName); + + } else if (manual) { + const char *sceneName = obs_source_get_name(source); + blog(LOG_INFO, "User started manual transition to scene '%s'", sceneName); + } + } + + if (force) { + obs_transition_set(transition, source); + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + } else { + int duration = ui->transitionDuration->value(); + + /* check for scene override */ + OBSSource trOverride = GetOverrideTransition(source); + + if (trOverride && !overridingTransition && !quickTransition) { + transition = std::move(trOverride); + duration = GetOverrideTransitionDuration(source); + OverrideTransition(transition.Get()); + overridingTransition = true; + } + + if (black && !prevFTBSource) { + prevFTBSource = source; + source = nullptr; + } else if (black && prevFTBSource) { + source = prevFTBSource; + prevFTBSource = nullptr; + } else if (!black) { + prevFTBSource = nullptr; + } + + if (quickTransition) + duration = quickDuration; + + enum obs_transition_mode mode = manual ? OBS_TRANSITION_MODE_MANUAL : OBS_TRANSITION_MODE_AUTO; + + EnableTransitionWidgets(false); + + bool success = obs_transition_start(transition, mode, duration, source); + + if (!success) + TransitionFullyStopped(); + } + +cleanup: + if (usingPreviewProgram && sceneDuplicationMode) + obs_scene_release(scene); +} + +static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr) +{ + int idx = combo->findData(QVariant::fromValue(tr)); + if (idx != -1) { + combo->blockSignals(true); + combo->setCurrentIndex(idx); + combo->blockSignals(false); + } +} + +void OBSBasic::SetTransition(OBSSource transition) +{ + OBSSourceAutoRelease oldTransition = obs_get_output_source(0); + + if (oldTransition && transition) { + obs_transition_swap_begin(transition, oldTransition); + if (transition != GetCurrentTransition()) + SetComboTransition(ui->transitions, transition); + obs_set_output_source(0, transition); + obs_transition_swap_end(transition, oldTransition); + } else { + obs_set_output_source(0, transition); + } + + bool fixed = transition ? obs_transition_fixed(transition) : false; + ui->transitionDurationLabel->setVisible(!fixed); + ui->transitionDuration->setVisible(!fixed); + + bool configurable = transition ? obs_source_configurable(transition) : false; + ui->transitionRemove->setEnabled(configurable); + ui->transitionProps->setEnabled(configurable); + + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_CHANGED); +} + +OBSSource OBSBasic::GetCurrentTransition() +{ + return ui->transitions->currentData().value(); +} + +void OBSBasic::on_transitions_currentIndexChanged(int) +{ + OBSSource transition = GetCurrentTransition(); + SetTransition(transition); +} + +void OBSBasic::AddTransition(const char *id) +{ + string name; + QString placeHolderText = QT_UTF8(obs_source_get_display_name(id)); + QString format = placeHolderText + " (%1)"; + obs_source_t *source = nullptr; + int i = 1; + + while ((FindTransition(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), + name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + AddTransition(id); + return; + } + + source = FindTransition(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + AddTransition(id); + return; + } + + source = obs_source_create_private(id, name.c_str(), NULL); + InitTransition(source); + ui->transitions->addItem(QT_UTF8(name.c_str()), QVariant::fromValue(OBSSource(source))); + ui->transitions->setCurrentIndex(ui->transitions->count() - 1); + CreatePropertiesWindow(source); + obs_source_release(source); + + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); + + ClearQuickTransitionWidgets(); + RefreshQuickTransitions(); + } +} + +void OBSBasic::on_transitionAdd_clicked() +{ + bool foundConfigurableTransitions = false; + QMenu menu(this); + size_t idx = 0; + const char *id; + + while (obs_enum_transition_types(idx++, &id)) { + if (obs_is_source_configurable(id)) { + const char *name = obs_source_get_display_name(id); + QAction *action = new QAction(name, this); + + connect(action, &QAction::triggered, [this, id]() { AddTransition(id); }); + + menu.addAction(action); + foundConfigurableTransitions = true; + } + } + + if (foundConfigurableTransitions) + menu.exec(QCursor::pos()); +} + +void OBSBasic::on_transitionRemove_clicked() +{ + OBSSource tr = GetCurrentTransition(); + + if (!tr || !obs_source_configurable(tr) || !QueryRemoveSource(tr)) + return; + + int idx = ui->transitions->findData(QVariant::fromValue(tr)); + if (idx == -1) + return; + + for (size_t i = quickTransitions.size(); i > 0; i--) { + QuickTransition &qt = quickTransitions[i - 1]; + if (qt.source == tr) { + if (qt.button) + qt.button->deleteLater(); + RemoveQuickTransitionHotkey(&qt); + quickTransitions.erase(quickTransitions.begin() + i - 1); + } + } + + ui->transitions->removeItem(idx); + + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); + + ClearQuickTransitionWidgets(); + RefreshQuickTransitions(); +} + +void OBSBasic::RenameTransition(OBSSource transition) +{ + string name; + QString placeHolderText = QT_UTF8(obs_source_get_name(transition)); + obs_source_t *source = nullptr; + + bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), + name, placeHolderText); + + if (!accepted) + return; + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + RenameTransition(transition); + return; + } + + source = FindTransition(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + RenameTransition(transition); + return; + } + + obs_source_set_name(transition, name.c_str()); + int idx = ui->transitions->findData(QVariant::fromValue(transition)); + if (idx != -1) { + ui->transitions->setItemText(idx, QT_UTF8(name.c_str())); + + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); + + ClearQuickTransitionWidgets(); + RefreshQuickTransitions(); + } +} + +void OBSBasic::on_transitionProps_clicked() +{ + OBSSource source = GetCurrentTransition(); + + if (!obs_source_configurable(source)) + return; + + auto properties = [&]() { + CreatePropertiesWindow(source); + }; + + QMenu menu(this); + + QAction *action = new QAction(QTStr("Rename"), &menu); + connect(action, &QAction::triggered, [this, source]() { RenameTransition(source); }); + menu.addAction(action); + + action = new QAction(QTStr("Properties"), &menu); + connect(action, &QAction::triggered, properties); + menu.addAction(action); + + menu.exec(QCursor::pos()); +} + +void OBSBasic::on_transitionDuration_valueChanged() +{ + OnEvent(OBS_FRONTEND_EVENT_TRANSITION_DURATION_CHANGED); +} + +QuickTransition *OBSBasic::GetQuickTransition(int id) +{ + for (QuickTransition &qt : quickTransitions) { + if (qt.id == id) + return &qt; + } + + return nullptr; +} + +int OBSBasic::GetQuickTransitionIdx(int id) +{ + for (int idx = 0; idx < (int)quickTransitions.size(); idx++) { + QuickTransition &qt = quickTransitions[idx]; + + if (qt.id == id) + return idx; + } + + return -1; +} + +void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force) +{ + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, force); +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +void OBSBasic::SetCurrentScene(OBSSource scene, bool force) +{ + if (!IsPreviewProgramMode()) { + TransitionToScene(scene, force); + } else { + OBSSource actualLastScene = OBSGetStrongRef(lastScene); + if (actualLastScene != scene) { + if (scene) + obs_source_inc_showing(scene); + if (actualLastScene) + obs_source_dec_showing(actualLastScene); + lastScene = OBSGetWeakRef(scene); + } + } + + if (obs_scene_get_source(GetCurrentScene()) != scene) { + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene itemScene = GetOBSRef(item); + obs_source_t *source = obs_scene_get_source(itemScene); + + if (source == scene) { + ui->scenes->blockSignals(true); + currentScene = itemScene.Get(); + ui->scenes->setCurrentItem(item); + ui->scenes->blockSignals(false); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + break; + } + } + } + + UpdateContextBar(true); + UpdatePreviewProgramIndicators(); + + if (scene) { + bool userSwitched = (!force && !disableSaving); + blog(LOG_INFO, "%s to scene '%s'", userSwitched ? "User switched" : "Switched", + obs_source_get_name(scene)); + } +} + +void OBSBasic::CreateProgramDisplay() +{ + program = new OBSQTDisplay(); + + program->setContextMenuPolicy(Qt::CustomContextMenu); + connect(program.data(), &QWidget::customContextMenuRequested, this, &OBSBasic::ProgramViewContextMenuRequested); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizeProgram(ovi.base_width, ovi.base_height); + }; + + connect(program.data(), &OBSQTDisplay::DisplayResized, displayResize); + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderProgram, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizeProgram(ovi.base_width, ovi.base_height); + }; + + connect(program.data(), &OBSQTDisplay::DisplayCreated, addDisplay); + + program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +void OBSBasic::TransitionClicked() +{ + if (previewProgramMode) + TransitionToScene(GetCurrentScene()); +} + +#define T_BAR_PRECISION 1024 +#define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) +#define T_BAR_CLAMP (T_BAR_PRECISION / 10) + +void OBSBasic::CreateProgramOptions() +{ + programOptions = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(); + layout->setSpacing(4); + + QPushButton *configTransitions = new QPushButton(); + configTransitions->setProperty("class", "icon-dots-vert"); + + QHBoxLayout *mainButtonLayout = new QHBoxLayout(); + mainButtonLayout->setSpacing(2); + + transitionButton = new QPushButton(QTStr("Transition")); + transitionButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + QHBoxLayout *quickTransitions = new QHBoxLayout(); + quickTransitions->setSpacing(2); + + QPushButton *addQuickTransition = new QPushButton(); + addQuickTransition->setProperty("class", "icon-plus"); + + QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions")); + quickTransitionsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + quickTransitions->addWidget(quickTransitionsLabel); + quickTransitions->addWidget(addQuickTransition); + + mainButtonLayout->addWidget(transitionButton); + mainButtonLayout->addWidget(configTransitions); + + tBar = new SliderIgnoreClick(Qt::Horizontal); + tBar->setMinimum(0); + tBar->setMaximum(T_BAR_PRECISION - 1); + + tBar->setProperty("class", "slider-tbar"); + + connect(tBar, &QSlider::valueChanged, this, &OBSBasic::TBarChanged); + connect(tBar, &QSlider::sliderReleased, this, &OBSBasic::TBarReleased); + + layout->addStretch(0); + layout->addLayout(mainButtonLayout); + layout->addLayout(quickTransitions); + layout->addWidget(tBar); + layout->addStretch(0); + + programOptions->setLayout(layout); + + auto onAdd = [this]() { + QScopedPointer menu(CreateTransitionMenu(this, nullptr)); + menu->exec(QCursor::pos()); + }; + + auto onConfig = [this]() { + QMenu menu(this); + QAction *action; + + auto toggleEditProperties = [this]() { + editPropertiesMode = !editPropertiesMode; + + OBSSource actualScene = OBSGetStrongRef(programScene); + if (actualScene) + TransitionToScene(actualScene, true); + }; + + auto toggleSwapScenesMode = [this]() { + swapScenesMode = !swapScenesMode; + }; + + auto toggleSceneDuplication = [this]() { + sceneDuplicationMode = !sceneDuplicationMode; + + OBSSource actualScene = OBSGetStrongRef(programScene); + if (actualScene) + TransitionToScene(actualScene, true); + }; + + auto showToolTip = [&]() { + QAction *act = menu.activeAction(); + QToolTip::showText(QCursor::pos(), act->toolTip(), &menu, menu.actionGeometry(act)); + }; + + action = menu.addAction(QTStr("QuickTransitions.DuplicateScene")); + action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT")); + action->setCheckable(true); + action->setChecked(sceneDuplicationMode); + connect(action, &QAction::triggered, toggleSceneDuplication); + connect(action, &QAction::hovered, showToolTip); + + action = menu.addAction(QTStr("QuickTransitions.EditProperties")); + action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT")); + action->setCheckable(true); + action->setChecked(editPropertiesMode); + action->setEnabled(sceneDuplicationMode); + connect(action, &QAction::triggered, toggleEditProperties); + connect(action, &QAction::hovered, showToolTip); + + action = menu.addAction(QTStr("QuickTransitions.SwapScenes")); + action->setToolTip(QTStr("QuickTransitions.SwapScenesTT")); + action->setCheckable(true); + action->setChecked(swapScenesMode); + connect(action, &QAction::triggered, toggleSwapScenesMode); + connect(action, &QAction::hovered, showToolTip); + + menu.exec(QCursor::pos()); + }; + + connect(transitionButton.data(), &QAbstractButton::clicked, this, &OBSBasic::TransitionClicked); + connect(addQuickTransition, &QAbstractButton::clicked, onAdd); + connect(configTransitions, &QAbstractButton::clicked, onConfig); +} + +void OBSBasic::TBarReleased() +{ + int val = tBar->value(); + + OBSSourceAutoRelease transition = obs_get_output_source(0); + + if ((tBar->maximum() - val) <= T_BAR_CLAMP) { + obs_transition_set_manual_time(transition, 1.0f); + tBar->blockSignals(true); + tBar->setValue(0); + tBar->blockSignals(false); + tBarActive = false; + EnableTransitionWidgets(true); + + OBSSourceAutoRelease target = obs_transition_get_active_source(transition); + const char *sceneName = obs_source_get_name(target); + blog(LOG_INFO, "Manual transition to scene '%s' finished", sceneName); + } else if (val <= T_BAR_CLAMP) { + obs_transition_set_manual_time(transition, 0.0f); + TransitionFullyStopped(); + tBar->blockSignals(true); + tBar->setValue(0); + tBar->blockSignals(false); + tBarActive = false; + EnableTransitionWidgets(true); + programScene = lastProgramScene; + blog(LOG_INFO, "Manual transition cancelled"); + } + + tBar->clearFocus(); +} + +static bool ValidTBarTransition(OBSSource transition) +{ + if (!transition) + return false; + + QString id = QT_UTF8(obs_source_get_id(transition)); + + if (id == "cut_transition" || id == "obs_stinger_transition") + return false; + + return true; +} + +void OBSBasic::TBarChanged(int value) +{ + OBSSourceAutoRelease transition = obs_get_output_source(0); + + if (!tBarActive) { + OBSSource sceneSource = GetCurrentSceneSource(); + OBSSource tBarTr = GetOverrideTransition(sceneSource); + + if (!ValidTBarTransition(tBarTr)) { + tBarTr = GetCurrentTransition(); + + if (!ValidTBarTransition(tBarTr)) + tBarTr = FindTransition(obs_source_get_display_name("fade_transition")); + + OverrideTransition(tBarTr); + overridingTransition = true; + + transition = std::move(tBarTr); + } + + obs_transition_set_manual_torque(transition, 8.0f, 0.05f); + TransitionToScene(sceneSource, false, false, false, 0, true); + tBarActive = true; + } + + obs_transition_set_manual_time(transition, (float)value / T_BAR_PRECISION_F); + + OnEvent(OBS_FRONTEND_EVENT_TBAR_VALUE_CHANGED); +} + +int OBSBasic::GetTbarPosition() +{ + return tBar->value(); +} + +void OBSBasic::TogglePreviewProgramMode() +{ + SetPreviewProgramMode(!IsPreviewProgramMode()); +} + +static inline void ResetQuickTransitionText(QuickTransition *qt) +{ + qt->button->setText(MakeQuickTransitionText(qt)); +} + +QMenu *OBSBasic::CreatePerSceneTransitionMenu() +{ + OBSSource scene = GetCurrentSceneSource(); + QMenu *menu = new QMenu(QTStr("TransitionOverride")); + QAction *action; + + OBSDataAutoRelease data = obs_source_get_private_settings(scene); + + obs_data_set_default_int(data, "transition_duration", 300); + + const char *curTransition = obs_data_get_string(data, "transition"); + int curDuration = (int)obs_data_get_int(data, "transition_duration"); + + QSpinBox *duration = new QSpinBox(menu); + duration->setMinimum(50); + duration->setSuffix(" ms"); + duration->setMaximum(20000); + duration->setSingleStep(50); + duration->setValue(curDuration); + + auto setTransition = [this](QAction *action) { + int idx = action->property("transition_index").toInt(); + OBSSource scene = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(scene); + + if (idx == -1) { + obs_data_set_string(data, "transition", ""); + return; + } + + OBSSource tr = GetTransitionComboItem(ui->transitions, idx); + + if (tr) { + const char *name = obs_source_get_name(tr); + obs_data_set_string(data, "transition", name); + } + }; + + auto setDuration = [this](int duration) { + OBSSource scene = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(scene); + + obs_data_set_int(data, "transition_duration", duration); + }; + + connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); + + for (int i = -1; i < ui->transitions->count(); i++) { + const char *name = ""; + + if (i >= 0) { + OBSSource tr; + tr = GetTransitionComboItem(ui->transitions, i); + if (!tr) + continue; + name = obs_source_get_name(tr); + } + + bool match = (name && strcmp(name, curTransition) == 0); + + if (!name || !*name) + name = Str("None"); + + action = menu->addAction(QT_UTF8(name)); + action->setProperty("transition_index", i); + action->setCheckable(true); + action->setChecked(match); + + connect(action, &QAction::triggered, std::bind(setTransition, action)); + } + + QWidgetAction *durationAction = new QWidgetAction(menu); + durationAction->setDefaultWidget(duration); + + menu->addSeparator(); + menu->addAction(durationAction); + return menu; +} + +void OBSBasic::ShowTransitionProperties() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_transition(item, true); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::HideTransitionProperties() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_transition(item, false); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration) +{ + int64_t sceneItemId = obs_sceneitem_get_id(item); + std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + auto undo_redo = [sceneUUID, sceneItemId, show](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); + if (i) { + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + obs_sceneitem_transition_load(i, dat, show); + } + }; + + OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(item, show); + + OBSSourceAutoRelease dup = obs_source_duplicate(tr, obs_source_get_name(tr), true); + obs_sceneitem_set_transition(item, show, dup); + obs_sceneitem_set_transition_duration(item, show, duration); + + OBSDataAutoRelease transitionData = obs_sceneitem_transition_save(item, show); + + std::string undo_data(obs_data_get_json(oldTransitionData)); + std::string redo_data(obs_data_get_json(transitionData)); + if (undo_data.compare(redo_data) == 0) + return; + + QString text = show ? QTStr("Undo.ShowTransition") : QTStr("Undo.HideTransition"); + const char *name = obs_source_get_name(obs_sceneitem_get_source(item)); + undo_s.add_action(text.arg(name), undo_redo, undo_redo, undo_data, redo_data); +} + +QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) +{ + OBSSceneItem si = GetCurrentSceneItem(); + + QMenu *menu = new QMenu(QTStr(visible ? "ShowTransition" : "HideTransition")); + QAction *action; + + OBSSource curTransition = obs_sceneitem_get_transition(si, visible); + const char *curId = curTransition ? obs_source_get_id(curTransition) : nullptr; + int curDuration = (int)obs_sceneitem_get_transition_duration(si, visible); + + if (curDuration <= 0) + curDuration = obs_frontend_get_transition_duration(); + + QSpinBox *duration = new QSpinBox(menu); + duration->setMinimum(50); + duration->setSuffix(" ms"); + duration->setMaximum(20000); + duration->setSingleStep(50); + duration->setValue(curDuration); + + auto setTransition = [this](QAction *action, bool visible) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + QString id = action->property("transition_id").toString(); + OBSSceneItem sceneItem = main->GetCurrentSceneItem(); + int64_t sceneItemId = obs_sceneitem_get_id(sceneItem); + std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(sceneItem))); + + auto undo_redo = [sceneUUID, sceneItemId, visible](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); + if (i) { + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + obs_sceneitem_transition_load(i, dat, visible); + } + }; + OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(sceneItem, visible); + if (id.isNull() || id.isEmpty()) { + obs_sceneitem_set_transition(sceneItem, visible, nullptr); + obs_sceneitem_set_transition_duration(sceneItem, visible, 0); + } else { + OBSSource tr = obs_sceneitem_get_transition(sceneItem, visible); + + if (!tr || strcmp(QT_TO_UTF8(id), obs_source_get_id(tr)) != 0) { + QString name = QT_UTF8(obs_source_get_name(obs_sceneitem_get_source(sceneItem))); + name += " "; + name += QTStr(visible ? "ShowTransition" : "HideTransition"); + tr = obs_source_create_private(QT_TO_UTF8(id), QT_TO_UTF8(name), nullptr); + obs_sceneitem_set_transition(sceneItem, visible, tr); + obs_source_release(tr); + + int duration = (int)obs_sceneitem_get_transition_duration(sceneItem, visible); + if (duration <= 0) { + duration = obs_frontend_get_transition_duration(); + obs_sceneitem_set_transition_duration(sceneItem, visible, duration); + } + } + if (obs_source_configurable(tr)) + CreatePropertiesWindow(tr); + } + OBSDataAutoRelease newTransitionData = obs_sceneitem_transition_save(sceneItem, visible); + std::string undo_data(obs_data_get_json(oldTransitionData)); + std::string redo_data(obs_data_get_json(newTransitionData)); + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr(visible ? "Undo.ShowTransition" : "Undo.HideTransition") + .arg(obs_source_get_name(obs_sceneitem_get_source(sceneItem))), + undo_redo, undo_redo, undo_data, redo_data); + }; + auto setDuration = [visible](int duration) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSceneItem item = main->GetCurrentSceneItem(); + obs_sceneitem_set_transition_duration(item, visible, duration); + }; + connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); + + action = menu->addAction(QT_UTF8(Str("None"))); + action->setProperty("transition_id", QT_UTF8("")); + action->setCheckable(true); + action->setChecked(!curId); + connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); + size_t idx = 0; + const char *id; + while (obs_enum_transition_types(idx++, &id)) { + const char *name = obs_source_get_display_name(id); + const bool match = id && curId && strcmp(id, curId) == 0; + action = menu->addAction(QT_UTF8(name)); + action->setProperty("transition_id", QT_UTF8(id)); + action->setCheckable(true); + action->setChecked(match); + connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); + } + + QWidgetAction *durationAction = new QWidgetAction(menu); + durationAction->setDefaultWidget(duration); + + menu->addSeparator(); + menu->addAction(durationAction); + if (curId && obs_is_source_configurable(curId)) { + menu->addSeparator(); + menu->addAction(QTStr("Properties"), this, + visible ? &OBSBasic::ShowTransitionProperties : &OBSBasic::HideTransitionProperties); + } + + auto copyTransition = [this](QAction *, bool visible) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSceneItem item = main->GetCurrentSceneItem(); + obs_source_t *tr = obs_sceneitem_get_transition(item, visible); + int trDur = obs_sceneitem_get_transition_duration(item, visible); + main->copySourceTransition = obs_source_get_weak_source(tr); + main->copySourceTransitionDuration = trDur; + }; + menu->addSeparator(); + action = menu->addAction(QT_UTF8(Str("Copy"))); + action->setEnabled(curId != nullptr); + connect(action, &QAction::triggered, std::bind(copyTransition, action, visible)); + + auto pasteTransition = [this](QAction *, bool show) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSource tr = OBSGetStrongRef(main->copySourceTransition); + int trDuration = main->copySourceTransitionDuration; + if (!tr) + return; + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = main->ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + PasteShowHideTransition(item, show, tr, trDuration); + } + }; + + action = menu->addAction(QT_UTF8(Str("Paste"))); + action->setEnabled(!!OBSGetStrongRef(copySourceTransition)); + connect(action, &QAction::triggered, std::bind(pasteTransition, action, visible)); + return menu; +} + +QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt) +{ + QMenu *menu = new QMenu(parent); + QAction *action; + OBSSource tr; + + if (qt) { + action = menu->addAction(QTStr("Remove")); + action->setProperty("id", qt->id); + connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionRemoveClicked); + + menu->addSeparator(); + } + + QSpinBox *duration = new QSpinBox(menu); + if (qt) + duration->setProperty("id", qt->id); + duration->setMinimum(50); + duration->setSuffix(" ms"); + duration->setMaximum(20000); + duration->setSingleStep(50); + duration->setValue(qt ? qt->duration : 300); + + if (qt) { + connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, this, + &OBSBasic::QuickTransitionChangeDuration); + } + + tr = fadeTransition; + + action = menu->addAction(QTStr("FadeToBlack")); + action->setProperty("fadeToBlack", true); + + if (qt) { + action->setProperty("id", qt->id); + connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); + } else { + action->setProperty("duration", QVariant::fromValue(duration)); + connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); + } + + for (int i = 0; i < ui->transitions->count(); i++) { + tr = GetTransitionComboItem(ui->transitions, i); + + if (!tr) + continue; + + action = menu->addAction(obs_source_get_name(tr)); + action->setProperty("transition_index", i); + + if (qt) { + action->setProperty("id", qt->id); + connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); + } else { + action->setProperty("duration", QVariant::fromValue(duration)); + connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); + } + } + + QWidgetAction *durationAction = new QWidgetAction(menu); + durationAction->setDefaultWidget(duration); + + menu->addSeparator(); + menu->addAction(durationAction); + return menu; +} + +void OBSBasic::AddQuickTransitionId(int id) +{ + QuickTransition *qt = GetQuickTransition(id); + if (!qt) + return; + + /* --------------------------------- */ + + QPushButton *button = new MenuButton(); + button->setProperty("id", id); + + qt->button = button; + ResetQuickTransitionText(qt); + + /* --------------------------------- */ + + QMenu *buttonMenu = CreateTransitionMenu(button, qt); + + /* --------------------------------- */ + + button->setMenu(buttonMenu); + connect(button, &QAbstractButton::clicked, this, &OBSBasic::QuickTransitionClicked); + + QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); + + int idx = 3; + for (;; idx++) { + QLayoutItem *item = programLayout->itemAt(idx); + if (!item) + break; + + QWidget *widget = item->widget(); + if (!widget || !widget->property("id").isValid()) + break; + } + + programLayout->insertWidget(idx, button); +} + +void OBSBasic::AddQuickTransition() +{ + int trIdx = sender()->property("transition_index").toInt(); + QSpinBox *duration = sender()->property("duration").value(); + bool fadeToBlack = sender()->property("fadeToBlack").value(); + OBSSource transition = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); + + if (!transition) + return; + + int id = quickTransitionIdCounter++; + + quickTransitions.emplace_back(transition, duration->value(), id, fadeToBlack); + AddQuickTransitionId(id); + + int idx = (int)quickTransitions.size() - 1; + AddQuickTransitionHotkey(&quickTransitions[idx]); +} + +void OBSBasic::ClearQuickTransitions() +{ + for (QuickTransition &qt : quickTransitions) + RemoveQuickTransitionHotkey(&qt); + quickTransitions.clear(); + + if (!programOptions) + return; + + QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); + + for (int idx = 0;; idx++) { + QLayoutItem *item = programLayout->itemAt(idx); + if (!item) + break; + + QWidget *widget = item->widget(); + if (!widget) + continue; + + int id = widget->property("id").toInt(); + if (id != 0) { + delete widget; + idx--; + } + } +} + +void OBSBasic::QuickTransitionClicked() +{ + int id = sender()->property("id").toInt(); + TriggerQuickTransition(id); +} + +void OBSBasic::QuickTransitionChange() +{ + int id = sender()->property("id").toInt(); + int trIdx = sender()->property("transition_index").toInt(); + bool fadeToBlack = sender()->property("fadeToBlack").value(); + QuickTransition *qt = GetQuickTransition(id); + + if (qt) { + OBSSource tr = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); + if (tr) { + qt->source = tr; + qt->fadeToBlack = fadeToBlack; + ResetQuickTransitionText(qt); + } + } +} + +void OBSBasic::QuickTransitionChangeDuration(int value) +{ + int id = sender()->property("id").toInt(); + QuickTransition *qt = GetQuickTransition(id); + + if (qt) { + qt->duration = value; + ResetQuickTransitionText(qt); + } +} + +void OBSBasic::QuickTransitionRemoveClicked() +{ + int id = sender()->property("id").toInt(); + int idx = GetQuickTransitionIdx(id); + if (idx == -1) + return; + + QuickTransition &qt = quickTransitions[idx]; + + if (qt.button) + qt.button->deleteLater(); + + RemoveQuickTransitionHotkey(&qt); + quickTransitions.erase(quickTransitions.begin() + idx); +} + +void OBSBasic::ClearQuickTransitionWidgets() +{ + if (!IsPreviewProgramMode()) + return; + + QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); + + for (int idx = 0;; idx++) { + QLayoutItem *item = programLayout->itemAt(idx); + if (!item) + break; + + QWidget *widget = item->widget(); + if (!widget) + continue; + + int id = widget->property("id").toInt(); + if (id != 0) { + delete widget; + idx--; + } + } +} + +void OBSBasic::RefreshQuickTransitions() +{ + if (!IsPreviewProgramMode()) + return; + + for (QuickTransition &qt : quickTransitions) + AddQuickTransitionId(qt.id); +} + +void OBSBasic::EnableTransitionWidgets(bool enable) +{ + ui->transitions->setEnabled(enable); + + if (!enable) { + ui->transitionProps->setEnabled(false); + } else { + bool configurable = obs_source_configurable(GetCurrentTransition()); + ui->transitionProps->setEnabled(configurable); + } + + if (!IsPreviewProgramMode()) + return; + + QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); + + for (int idx = 0;; idx++) { + QLayoutItem *item = programLayout->itemAt(idx); + if (!item) + break; + + QPushButton *button = qobject_cast(item->widget()); + if (!button) + continue; + + button->setEnabled(enable); + } + + if (transitionButton) + transitionButton->setEnabled(enable); +} + +void OBSBasic::SetPreviewProgramMode(bool enabled) +{ + if (IsPreviewProgramMode() == enabled) + return; + + os_atomic_set_bool(&previewProgramMode, enabled); + emit PreviewProgramModeChanged(enabled); + + if (IsPreviewProgramMode()) { + if (!previewEnabled) + EnablePreviewDisplay(true); + + CreateProgramDisplay(); + CreateProgramOptions(); + + OBSScene curScene = GetCurrentScene(); + + OBSSceneAutoRelease dup; + if (sceneDuplicationMode) { + dup = obs_scene_duplicate(curScene, obs_source_get_name(obs_scene_get_source(curScene)), + editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY + : OBS_SCENE_DUP_PRIVATE_REFS); + } else { + dup = std::move(OBSScene(curScene)); + } + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *dup_source = obs_scene_get_source(dup); + obs_transition_set(transition, dup_source); + + if (curScene) { + obs_source_t *source = obs_scene_get_source(curScene); + obs_source_inc_showing(source); + lastScene = OBSGetWeakRef(source); + programScene = OBSGetWeakRef(source); + } + + RefreshQuickTransitions(); + + programLabel = new QLabel(QTStr("StudioMode.ProgramSceneLabel"), this); + programLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + programLabel->setProperty("class", "label-preview-title"); + + programWidget = new QWidget(); + programLayout = new QVBoxLayout(); + + programLayout->setContentsMargins(0, 0, 0, 0); + programLayout->setSpacing(0); + + programLayout->addWidget(programLabel); + programLayout->addWidget(program); + + programWidget->setLayout(programLayout); + + ui->previewLayout->addWidget(programOptions); + ui->previewLayout->addWidget(programWidget); + ui->previewLayout->setAlignment(programOptions, Qt::AlignCenter); + + OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED); + + blog(LOG_INFO, "Switched to Preview/Program mode"); + blog(LOG_INFO, "-----------------------------" + "-------------------"); + } else { + OBSSource actualProgramScene = OBSGetStrongRef(programScene); + if (!actualProgramScene) + actualProgramScene = GetCurrentSceneSource(); + else + SetCurrentScene(actualProgramScene, true); + TransitionToScene(actualProgramScene, true); + + delete programOptions; + delete program; + delete programLabel; + delete programWidget; + + if (lastScene) { + OBSSource actualLastScene = OBSGetStrongRef(lastScene); + if (actualLastScene) + obs_source_dec_showing(actualLastScene); + lastScene = nullptr; + } + + programScene = nullptr; + swapScene = nullptr; + prevFTBSource = nullptr; + + for (QuickTransition &qt : quickTransitions) + qt.button = nullptr; + + if (!previewEnabled) + EnablePreviewDisplay(false); + + ui->transitions->setEnabled(true); + tBarActive = false; + + OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED); + + blog(LOG_INFO, "Switched to regular Preview mode"); + blog(LOG_INFO, "-----------------------------" + "-------------------"); + } + + ResetUI(); + UpdateTitleBar(); +} + +void OBSBasic::RenderProgram(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderProgram"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->programCX = int(window->programScale * float(ovi.base_width)); + window->programCY = int(window->programScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->programX, window->programY, window->programCX, window->programCY); + + obs_render_main_texture_src_color_only(); + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + + /* resize program panel to fix to the top section of the window */ + targetSize = GetPixelSize(program); + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, programX, programY, programScale); + + programX += float(PREVIEW_EDGE_SIZE); + programY += float(PREVIEW_EDGE_SIZE); +} + +obs_data_array_t *OBSBasic::SaveTransitions() +{ + obs_data_array_t *transitions = obs_data_array_create(); + + for (int i = 0; i < ui->transitions->count(); i++) { + OBSSource tr = ui->transitions->itemData(i).value(); + if (!tr || !obs_source_configurable(tr)) + continue; + + OBSDataAutoRelease sourceData = obs_data_create(); + OBSDataAutoRelease settings = obs_source_get_settings(tr); + + obs_data_set_string(sourceData, "name", obs_source_get_name(tr)); + obs_data_set_string(sourceData, "id", obs_obj_get_id(tr)); + obs_data_set_obj(sourceData, "settings", settings); + + obs_data_array_push_back(transitions, sourceData); + } + + for (const OBSDataAutoRelease &transition : safeModeTransitions) { + obs_data_array_push_back(transitions, transition); + } + + return transitions; +} + +void OBSBasic::LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data) +{ + size_t count = obs_data_array_count(transitions); + + safeModeTransitions.clear(); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease item = obs_data_array_item(transitions, i); + const char *name = obs_data_get_string(item, "name"); + const char *id = obs_data_get_string(item, "id"); + OBSDataAutoRelease settings = obs_data_get_obj(item, "settings"); + + OBSSourceAutoRelease source = obs_source_create_private(id, name, settings); + if (!obs_obj_invalid(source)) { + InitTransition(source); + + ui->transitions->addItem(QT_UTF8(name), QVariant::fromValue(OBSSource(source))); + ui->transitions->setCurrentIndex(ui->transitions->count() - 1); + if (cb) + cb(private_data, source); + } else if (safe_mode || disable_3p_plugins) { + safeModeTransitions.push_back(std::move(item)); + } + } +} + +OBSSource OBSBasic::GetOverrideTransition(OBSSource source) +{ + if (!source) + return nullptr; + + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + const char *trOverrideName = obs_data_get_string(data, "transition"); + + OBSSource trOverride = nullptr; + + if (trOverrideName && *trOverrideName) + trOverride = FindTransition(trOverrideName); + + return trOverride; +} + +int OBSBasic::GetOverrideTransitionDuration(OBSSource source) +{ + if (!source) + return 300; + + OBSDataAutoRelease data = obs_source_get_private_settings(source); + obs_data_set_default_int(data, "transition_duration", 300); + + return (int)obs_data_get_int(data, "transition_duration"); +} + +void OBSBasic::UpdatePreviewProgramIndicators() +{ + bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") + : false; + + ui->previewLabel->setVisible(labels); + + if (programLabel) + programLabel->setVisible(labels); + + if (!labels) + return; + + QString preview = + QTStr("StudioMode.PreviewSceneName").arg(QT_UTF8(obs_source_get_name(GetCurrentSceneSource()))); + + QString program = QTStr("StudioMode.ProgramSceneName").arg(QT_UTF8(obs_source_get_name(GetProgramSource()))); + + if (ui->previewLabel->text() != preview) + ui->previewLabel->setText(preview); + + if (programLabel && programLabel->text() != program) + programLabel->setText(program); +} From d846b0ba0a27b09c0af289e9cba041b2dcaf64be Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 04:02:57 +0100 Subject: [PATCH 18/37] frontend: Add renamed UI utility classes and functions --- .../utility/AutoUpdateThread.cpp | 34 ++++------ .../utility/AutoUpdateThread.hpp | 1 - .../utility/GoLiveAPI_CensoredJson.cpp | 6 +- .../utility/GoLiveAPI_CensoredJson.hpp | 1 + .../utility/GoLiveAPI_Network.cpp | 17 ++--- .../utility/GoLiveAPI_Network.hpp | 7 +- .../utility/GoLiveAPI_PostData.cpp | 9 ++- .../utility/GoLiveAPI_PostData.hpp | 9 ++- .../utility/MultitrackVideoError.cpp | 5 +- .../utility/MultitrackVideoError.hpp | 1 + .../utility/MultitrackVideoOutput.cpp | 43 +++--------- .../utility/MultitrackVideoOutput.hpp | 6 +- .../utility/OBSProxyStyle.cpp | 5 +- .../utility/OBSProxyStyle.hpp | 0 .../utility/RemoteTextThread.cpp | 11 ++- .../utility/RemoteTextThread.hpp | 2 - .../utility/ScreenshotObj.hpp | 8 ++- .../utility/VCamConfig.hpp | 2 - .../utility/YoutubeApiWrappers.cpp | 21 +++--- .../utility/YoutubeApiWrappers.hpp | 3 +- {UI => frontend/utility}/audio-encoders.cpp | 15 ++--- {UI => frontend/utility}/audio-encoders.hpp | 3 +- .../utility}/crypto-helpers-mac.mm | 0 .../utility}/crypto-helpers-mbedtls.cpp | 4 +- .../utility}/crypto-helpers.hpp | 2 +- {UI => frontend/utility}/display-helpers.hpp | 6 +- .../utility}/item-widget-helpers.cpp | 2 + .../utility}/item-widget-helpers.hpp | 2 + .../utility}/models/branches.hpp | 0 .../utility}/models/multitrack-video.hpp | 0 .../utility}/models/whatsnew.hpp | 0 {UI => frontend/utility}/obf.c | 1 + {UI => frontend/utility}/obf.h | 0 {UI => frontend/utility}/platform-osx.mm | 17 ++--- {UI => frontend/utility}/platform-windows.cpp | 30 ++++----- {UI => frontend/utility}/platform-x11.cpp | 67 +++++++++++-------- {UI => frontend/utility}/platform.hpp | 0 {UI => frontend/utility}/system-info-macos.mm | 0 .../utility}/system-info-posix.cpp | 0 .../utility}/system-info-windows.cpp | 9 +-- {UI => frontend/utility}/system-info.hpp | 0 .../utility/undo_stack.cpp | 6 +- .../utility/undo_stack.hpp | 6 +- .../utility}/update-helpers.cpp | 0 .../utility}/update-helpers.hpp | 1 - {UI => frontend/utility}/win-dll-blocklist.c | 0 46 files changed, 175 insertions(+), 187 deletions(-) rename UI/update/win-update.cpp => frontend/utility/AutoUpdateThread.cpp (96%) rename UI/update/win-update.hpp => frontend/utility/AutoUpdateThread.hpp (97%) rename UI/goliveapi-censoredjson.cpp => frontend/utility/GoLiveAPI_CensoredJson.cpp (98%) rename UI/goliveapi-censoredjson.hpp => frontend/utility/GoLiveAPI_CensoredJson.hpp (99%) rename UI/goliveapi-network.cpp => frontend/utility/GoLiveAPI_Network.cpp (95%) rename UI/goliveapi-network.hpp => frontend/utility/GoLiveAPI_Network.hpp (99%) rename UI/goliveapi-postdata.cpp => frontend/utility/GoLiveAPI_PostData.cpp (96%) rename UI/goliveapi-postdata.hpp => frontend/utility/GoLiveAPI_PostData.hpp (99%) rename UI/multitrack-video-error.cpp => frontend/utility/MultitrackVideoError.cpp (94%) rename UI/multitrack-video-error.hpp => frontend/utility/MultitrackVideoError.hpp (99%) rename UI/multitrack-video-output.cpp => frontend/utility/MultitrackVideoOutput.cpp (97%) rename UI/multitrack-video-output.hpp => frontend/utility/MultitrackVideoOutput.hpp (97%) rename UI/obs-proxy-style.cpp => frontend/utility/OBSProxyStyle.cpp (97%) rename UI/obs-proxy-style.hpp => frontend/utility/OBSProxyStyle.hpp (100%) rename UI/remote-text.cpp => frontend/utility/RemoteTextThread.cpp (98%) rename UI/remote-text.hpp => frontend/utility/RemoteTextThread.hpp (98%) rename UI/screenshot-obj.hpp => frontend/utility/ScreenshotObj.hpp (98%) rename UI/window-basic-vcam.hpp => frontend/utility/VCamConfig.hpp (95%) rename UI/youtube-api-wrappers.cpp => frontend/utility/YoutubeApiWrappers.cpp (98%) rename UI/youtube-api-wrappers.hpp => frontend/utility/YoutubeApiWrappers.hpp (98%) rename {UI => frontend/utility}/audio-encoders.cpp (98%) rename {UI => frontend/utility}/audio-encoders.hpp (96%) rename {UI/update => frontend/utility}/crypto-helpers-mac.mm (100%) rename {UI/update => frontend/utility}/crypto-helpers-mbedtls.cpp (93%) rename {UI/update => frontend/utility}/crypto-helpers.hpp (100%) rename {UI => frontend/utility}/display-helpers.hpp (98%) rename {UI => frontend/utility}/item-widget-helpers.cpp (97%) rename {UI => frontend/utility}/item-widget-helpers.hpp (98%) rename {UI/update => frontend/utility}/models/branches.hpp (100%) rename {UI => frontend/utility}/models/multitrack-video.hpp (100%) rename {UI/update => frontend/utility}/models/whatsnew.hpp (100%) rename {UI => frontend/utility}/obf.c (99%) rename {UI => frontend/utility}/obf.h (100%) rename {UI => frontend/utility}/platform-osx.mm (97%) rename {UI => frontend/utility}/platform-windows.cpp (98%) rename {UI => frontend/utility}/platform-x11.cpp (94%) rename {UI => frontend/utility}/platform.hpp (100%) rename {UI => frontend/utility}/system-info-macos.mm (100%) rename {UI => frontend/utility}/system-info-posix.cpp (100%) rename {UI => frontend/utility}/system-info-windows.cpp (99%) rename {UI => frontend/utility}/system-info.hpp (100%) rename UI/undo-stack-obs.cpp => frontend/utility/undo_stack.cpp (97%) rename UI/undo-stack-obs.hpp => frontend/utility/undo_stack.hpp (96%) rename {UI/update => frontend/utility}/update-helpers.cpp (100%) rename {UI/update => frontend/utility}/update-helpers.hpp (81%) rename {UI => frontend/utility}/win-dll-blocklist.c (100%) diff --git a/UI/update/win-update.cpp b/frontend/utility/AutoUpdateThread.cpp similarity index 96% rename from UI/update/win-update.cpp rename to frontend/utility/AutoUpdateThread.cpp index 73ef8e696..f72e56f77 100644 --- a/UI/update/win-update.cpp +++ b/frontend/utility/AutoUpdateThread.cpp @@ -1,30 +1,19 @@ -#include "../win-update/updater/manifest.hpp" -#include "update-helpers.hpp" -#include "shared-update.hpp" -#include "update-window.hpp" -#include "remote-text.hpp" -#include "win-update.hpp" -#include "obs-app.hpp" +#include "AutoUpdateThread.hpp" +#include "ui_OBSUpdate.h" + +#include +#include +#include +#include +#include #include -#include - -#include -#include #define WIN32_LEAN_AND_MEAN #include #include -#include -#include - -#ifdef BROWSER_AVAILABLE -#include -#endif - -using namespace std; -using namespace updater; +#include "moc_AutoUpdateThread.cpp" /* ------------------------------------------------------------------------ */ @@ -50,6 +39,11 @@ using namespace updater; /* ------------------------------------------------------------------------ */ +using namespace std; +using namespace updater; + +extern char *GetConfigPathPtr(const char *name); + static bool ParseUpdateManifest(const char *manifest_data, bool *updatesAvailable, string ¬es, string &updateVer, const string &branch) try { diff --git a/UI/update/win-update.hpp b/frontend/utility/AutoUpdateThread.hpp similarity index 97% rename from UI/update/win-update.hpp rename to frontend/utility/AutoUpdateThread.hpp index d345f204b..a0b3b0b4c 100644 --- a/UI/update/win-update.hpp +++ b/frontend/utility/AutoUpdateThread.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include class AutoUpdateThread : public QThread { Q_OBJECT diff --git a/UI/goliveapi-censoredjson.cpp b/frontend/utility/GoLiveAPI_CensoredJson.cpp similarity index 98% rename from UI/goliveapi-censoredjson.cpp rename to frontend/utility/GoLiveAPI_CensoredJson.cpp index bd59e5d1e..fc14bd372 100644 --- a/UI/goliveapi-censoredjson.cpp +++ b/frontend/utility/GoLiveAPI_CensoredJson.cpp @@ -1,7 +1,9 @@ -#include "goliveapi-censoredjson.hpp" -#include +#include "GoLiveAPI_CensoredJson.hpp" + #include +#include + void censorRecurse(obs_data_t *); void censorRecurseArray(obs_data_array_t *); diff --git a/UI/goliveapi-censoredjson.hpp b/frontend/utility/GoLiveAPI_CensoredJson.hpp similarity index 99% rename from UI/goliveapi-censoredjson.hpp rename to frontend/utility/GoLiveAPI_CensoredJson.hpp index 20f141c7f..004eace63 100644 --- a/UI/goliveapi-censoredjson.hpp +++ b/frontend/utility/GoLiveAPI_CensoredJson.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include #include diff --git a/UI/goliveapi-network.cpp b/frontend/utility/GoLiveAPI_Network.cpp similarity index 95% rename from UI/goliveapi-network.cpp rename to frontend/utility/GoLiveAPI_Network.cpp index dc557bbc1..b0688a006 100644 --- a/UI/goliveapi-network.cpp +++ b/frontend/utility/GoLiveAPI_Network.cpp @@ -1,17 +1,18 @@ -#include "goliveapi-network.hpp" -#include "goliveapi-censoredjson.hpp" +#include "GoLiveAPI_Network.hpp" +#include "GoLiveAPI_CensoredJson.hpp" + +#include +#include +#include #include -#include -#include -#include "multitrack-video-error.hpp" -#include -#include #include #include - #include +#include + +#include using json = nlohmann::json; diff --git a/UI/goliveapi-network.hpp b/frontend/utility/GoLiveAPI_Network.hpp similarity index 99% rename from UI/goliveapi-network.hpp rename to frontend/utility/GoLiveAPI_Network.hpp index 2a2daecf8..451526068 100644 --- a/UI/goliveapi-network.hpp +++ b/frontend/utility/GoLiveAPI_Network.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include - #include "models/multitrack-video.hpp" +#include + +#include + /** Returns either GO_LIVE_API_PRODUCTION_URL or a command line override. */ QString MultitrackVideoAutoConfigURL(obs_service_t *service); diff --git a/UI/goliveapi-postdata.cpp b/frontend/utility/GoLiveAPI_PostData.cpp similarity index 96% rename from UI/goliveapi-postdata.cpp rename to frontend/utility/GoLiveAPI_PostData.cpp index 9056764db..1aeb871a7 100644 --- a/UI/goliveapi-postdata.cpp +++ b/frontend/utility/GoLiveAPI_PostData.cpp @@ -1,11 +1,10 @@ -#include "goliveapi-postdata.hpp" +#include "GoLiveAPI_PostData.hpp" +#include "models/multitrack-video.hpp" + +#include #include -#include "system-info.hpp" - -#include "models/multitrack-video.hpp" - GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled) { diff --git a/UI/goliveapi-postdata.hpp b/frontend/utility/GoLiveAPI_PostData.hpp similarity index 99% rename from UI/goliveapi-postdata.hpp rename to frontend/utility/GoLiveAPI_PostData.hpp index 23f010883..42c9c921f 100644 --- a/UI/goliveapi-postdata.hpp +++ b/frontend/utility/GoLiveAPI_PostData.hpp @@ -1,9 +1,12 @@ #pragma once -#include -#include -#include #include "models/multitrack-video.hpp" +#include + +#include + +#include + GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled); diff --git a/UI/multitrack-video-error.cpp b/frontend/utility/MultitrackVideoError.cpp similarity index 94% rename from UI/multitrack-video-error.cpp rename to frontend/utility/MultitrackVideoError.cpp index 8f4b478b4..5f7d95b1c 100644 --- a/UI/multitrack-video-error.cpp +++ b/frontend/utility/MultitrackVideoError.cpp @@ -1,8 +1,9 @@ -#include "multitrack-video-error.hpp" +#include "MultitrackVideoError.hpp" + +#include #include #include -#include "obs-app.hpp" MultitrackVideoError MultitrackVideoError::critical(QString error) { diff --git a/UI/multitrack-video-error.hpp b/frontend/utility/MultitrackVideoError.hpp similarity index 99% rename from UI/multitrack-video-error.hpp rename to frontend/utility/MultitrackVideoError.hpp index 7ce79fe72..b36f2f274 100644 --- a/UI/multitrack-video-error.hpp +++ b/frontend/utility/MultitrackVideoError.hpp @@ -1,4 +1,5 @@ #pragma once + #include class QWidget; diff --git a/UI/multitrack-video-output.cpp b/frontend/utility/MultitrackVideoOutput.cpp similarity index 97% rename from UI/multitrack-video-output.cpp rename to frontend/utility/MultitrackVideoOutput.cpp index 2a54505ee..1ee0fe170 100644 --- a/UI/multitrack-video-output.cpp +++ b/frontend/utility/MultitrackVideoOutput.cpp @@ -1,42 +1,21 @@ -#include "multitrack-video-output.hpp" +#include "MultitrackVideoError.hpp" +#include "MultitrackVideoOutput.hpp" +#include "models/multitrack-video.hpp" +#include "GoLiveAPI_Network.hpp" +#include "GoLiveAPI_PostData.hpp" + +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include +#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include #include -#include -#include -#include +#include +#include #include #include -#include -#include - -#include "system-info.hpp" -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" -#include "models/multitrack-video.hpp" +#include Qt::ConnectionType BlockingConnectionTypeFor(QObject *object) { diff --git a/UI/multitrack-video-output.hpp b/frontend/utility/MultitrackVideoOutput.hpp similarity index 97% rename from UI/multitrack-video-output.hpp rename to frontend/utility/MultitrackVideoOutput.hpp index 024cd8bca..851ef3562 100644 --- a/UI/multitrack-video-output.hpp +++ b/frontend/utility/MultitrackVideoOutput.hpp @@ -3,16 +3,16 @@ #include #include -#include +#include #include #include +#include #include -#include - #define NOMINMAX class QString; +class QWidget; void StreamStopHandler(void *arg, calldata_t *data); void StreamDeactivateHandler(void *arg, calldata_t *data); diff --git a/UI/obs-proxy-style.cpp b/frontend/utility/OBSProxyStyle.cpp similarity index 97% rename from UI/obs-proxy-style.cpp rename to frontend/utility/OBSProxyStyle.cpp index 4daf7f47a..e598e1938 100644 --- a/UI/obs-proxy-style.cpp +++ b/frontend/utility/OBSProxyStyle.cpp @@ -1,5 +1,6 @@ -#include "obs-proxy-style.hpp" -#include +#include "OBSProxyStyle.hpp" + +#include static inline uint qt_intensity(uint r, uint g, uint b) { diff --git a/UI/obs-proxy-style.hpp b/frontend/utility/OBSProxyStyle.hpp similarity index 100% rename from UI/obs-proxy-style.hpp rename to frontend/utility/OBSProxyStyle.hpp diff --git a/UI/remote-text.cpp b/frontend/utility/RemoteTextThread.cpp similarity index 98% rename from UI/remote-text.cpp rename to frontend/utility/RemoteTextThread.cpp index 4b81e32bb..9541fd439 100644 --- a/UI/remote-text.cpp +++ b/frontend/utility/RemoteTextThread.cpp @@ -15,16 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "RemoteTextThread.hpp" + +#include + #include -#include "obs-app.hpp" -#include "moc_remote-text.cpp" +#include + +#include "moc_RemoteTextThread.cpp" using namespace std; static auto curl_deleter = [](CURL *curl) { curl_easy_cleanup(curl); }; + using Curl = unique_ptr; static size_t string_write(char *ptr, size_t size, size_t nmemb, string &str) diff --git a/UI/remote-text.hpp b/frontend/utility/RemoteTextThread.hpp similarity index 98% rename from UI/remote-text.hpp rename to frontend/utility/RemoteTextThread.hpp index f6f8efbc1..af24a51bc 100644 --- a/UI/remote-text.hpp +++ b/frontend/utility/RemoteTextThread.hpp @@ -18,8 +18,6 @@ #pragma once #include -#include -#include class RemoteTextThread : public QThread { Q_OBJECT diff --git a/UI/screenshot-obj.hpp b/frontend/utility/ScreenshotObj.hpp similarity index 98% rename from UI/screenshot-obj.hpp rename to frontend/utility/ScreenshotObj.hpp index 037b7f84d..01a4fefd8 100644 --- a/UI/screenshot-obj.hpp +++ b/frontend/utility/ScreenshotObj.hpp @@ -17,11 +17,13 @@ #pragma once -#include -#include -#include #include +#include +#include + +#include + class ScreenshotObj : public QObject { Q_OBJECT diff --git a/UI/window-basic-vcam.hpp b/frontend/utility/VCamConfig.hpp similarity index 95% rename from UI/window-basic-vcam.hpp rename to frontend/utility/VCamConfig.hpp index a3a5b6a2f..a3fa42953 100644 --- a/UI/window-basic-vcam.hpp +++ b/frontend/utility/VCamConfig.hpp @@ -1,7 +1,5 @@ #pragma once -#include - constexpr const char *VIRTUAL_CAM_ID = "virtualcam_output"; enum VCamOutputType { diff --git a/UI/youtube-api-wrappers.cpp b/frontend/utility/YoutubeApiWrappers.cpp similarity index 98% rename from UI/youtube-api-wrappers.cpp rename to frontend/utility/YoutubeApiWrappers.cpp index 1ed397f04..fba625495 100644 --- a/UI/youtube-api-wrappers.cpp +++ b/frontend/utility/YoutubeApiWrappers.cpp @@ -1,19 +1,16 @@ -#include "moc_youtube-api-wrappers.cpp" +#include "YoutubeApiWrappers.hpp" -#include -#include -#include +#include +#include +#include -#include -#include #include +#include -#include "auth-youtube.hpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" +#include +#include + +#include "moc_YoutubeApiWrappers.cpp" using namespace json11; diff --git a/UI/youtube-api-wrappers.hpp b/frontend/utility/YoutubeApiWrappers.hpp similarity index 98% rename from UI/youtube-api-wrappers.hpp rename to frontend/utility/YoutubeApiWrappers.hpp index 5216d2eb2..9cabb7091 100644 --- a/UI/youtube-api-wrappers.hpp +++ b/frontend/utility/YoutubeApiWrappers.hpp @@ -1,8 +1,9 @@ #pragma once -#include "auth-youtube.hpp" +#include #include + #include struct ChannelDescription { diff --git a/UI/audio-encoders.cpp b/frontend/utility/audio-encoders.cpp similarity index 98% rename from UI/audio-encoders.cpp rename to frontend/utility/audio-encoders.cpp index 692f25719..357ec45ca 100644 --- a/UI/audio-encoders.cpp +++ b/frontend/utility/audio-encoders.cpp @@ -1,15 +1,10 @@ -#include -#include -#include -#include +#include "audio-encoders.hpp" + +#include +#include + #include #include -#include -#include - -#include "audio-encoders.hpp" -#include "obs-app.hpp" -#include "window-main.hpp" using namespace std; diff --git a/UI/audio-encoders.hpp b/frontend/utility/audio-encoders.hpp similarity index 96% rename from UI/audio-encoders.hpp rename to frontend/utility/audio-encoders.hpp index b7fd0ab10..4a5f1b15c 100644 --- a/UI/audio-encoders.hpp +++ b/frontend/utility/audio-encoders.hpp @@ -1,8 +1,7 @@ #pragma once -#include - #include +#include #include const std::map &GetSimpleAACEncoderBitrateMap(); diff --git a/UI/update/crypto-helpers-mac.mm b/frontend/utility/crypto-helpers-mac.mm similarity index 100% rename from UI/update/crypto-helpers-mac.mm rename to frontend/utility/crypto-helpers-mac.mm diff --git a/UI/update/crypto-helpers-mbedtls.cpp b/frontend/utility/crypto-helpers-mbedtls.cpp similarity index 93% rename from UI/update/crypto-helpers-mbedtls.cpp rename to frontend/utility/crypto-helpers-mbedtls.cpp index f17c98764..caaef80dc 100644 --- a/UI/update/crypto-helpers-mbedtls.cpp +++ b/frontend/utility/crypto-helpers-mbedtls.cpp @@ -1,7 +1,7 @@ #include "crypto-helpers.hpp" -#include "mbedtls/md.h" -#include "mbedtls/pk.h" +#include +#include bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, const uint8_t *buf, const size_t len, const uint8_t *sig, const size_t sigLen) diff --git a/UI/update/crypto-helpers.hpp b/frontend/utility/crypto-helpers.hpp similarity index 100% rename from UI/update/crypto-helpers.hpp rename to frontend/utility/crypto-helpers.hpp index 8fe9c594e..890ec40c8 100644 --- a/UI/update/crypto-helpers.hpp +++ b/frontend/utility/crypto-helpers.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, const uint8_t *buf, const size_t len, const uint8_t *sig, const size_t sigLen); diff --git a/UI/display-helpers.hpp b/frontend/utility/display-helpers.hpp similarity index 98% rename from UI/display-helpers.hpp rename to frontend/utility/display-helpers.hpp index 586cbfeb6..a3b5d4457 100644 --- a/UI/display-helpers.hpp +++ b/frontend/utility/display-helpers.hpp @@ -17,8 +17,12 @@ #pragma once -#include #include +#include +#include + +#include +#include static inline void GetScaleAndCenterPos(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float &scale) diff --git a/UI/item-widget-helpers.cpp b/frontend/utility/item-widget-helpers.cpp similarity index 97% rename from UI/item-widget-helpers.cpp rename to frontend/utility/item-widget-helpers.cpp index cd8ee8dc6..f6477e7e0 100644 --- a/UI/item-widget-helpers.cpp +++ b/frontend/utility/item-widget-helpers.cpp @@ -15,6 +15,8 @@ along with this program. If not, see . ******************************************************************************/ +#include "item-widget-helpers.hpp" + #include QListWidgetItem *TakeListItem(QListWidget *widget, int row) diff --git a/UI/item-widget-helpers.hpp b/frontend/utility/item-widget-helpers.hpp similarity index 98% rename from UI/item-widget-helpers.hpp rename to frontend/utility/item-widget-helpers.hpp index 599b83125..ae18ec7a8 100644 --- a/UI/item-widget-helpers.hpp +++ b/frontend/utility/item-widget-helpers.hpp @@ -22,6 +22,8 @@ * such as references to sources/etc from getting stuck in the Qt event queue * with no way of controlling when they'll be released. */ +#include + class QListWidget; class QListWidgetItem; diff --git a/UI/update/models/branches.hpp b/frontend/utility/models/branches.hpp similarity index 100% rename from UI/update/models/branches.hpp rename to frontend/utility/models/branches.hpp diff --git a/UI/models/multitrack-video.hpp b/frontend/utility/models/multitrack-video.hpp similarity index 100% rename from UI/models/multitrack-video.hpp rename to frontend/utility/models/multitrack-video.hpp diff --git a/UI/update/models/whatsnew.hpp b/frontend/utility/models/whatsnew.hpp similarity index 100% rename from UI/update/models/whatsnew.hpp rename to frontend/utility/models/whatsnew.hpp diff --git a/UI/obf.c b/frontend/utility/obf.c similarity index 99% rename from UI/obf.c rename to frontend/utility/obf.c index feba1886e..f144e76fc 100644 --- a/UI/obf.c +++ b/frontend/utility/obf.c @@ -1,4 +1,5 @@ #include "obf.h" + #include #define LOWER_HALFBYTE(x) ((x) & 0xF) diff --git a/UI/obf.h b/frontend/utility/obf.h similarity index 100% rename from UI/obf.h rename to frontend/utility/obf.h diff --git a/UI/platform-osx.mm b/frontend/utility/platform-osx.mm similarity index 97% rename from UI/platform-osx.mm rename to frontend/utility/platform-osx.mm index 75f4adfba..c9648b00f 100644 --- a/UI/platform-osx.mm +++ b/frontend/utility/platform-osx.mm @@ -15,20 +15,15 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include "platform.hpp" -#include "obs-app.hpp" +#import "platform.hpp" -#include +#import + +#import -#import -#import #import -#import +#import +#import using namespace std; diff --git a/UI/platform-windows.cpp b/frontend/utility/platform-windows.cpp similarity index 98% rename from UI/platform-windows.cpp rename to frontend/utility/platform-windows.cpp index d5d76ccc4..c6256b28c 100644 --- a/UI/platform-windows.cpp +++ b/frontend/utility/platform-windows.cpp @@ -15,30 +15,28 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include "obs-config.h" -#include "obs-app.hpp" #include "platform.hpp" -#include -#include -#include +#include -#define WIN32_LEAN_AND_MEAN -#include +#include +#include +#include + +#include +#include +#include #include #include -#include -#include -#include - -#include -#include -#include +#include +#define WIN32_LEAN_AND_MEAN +#include using namespace std; +extern bool portable_mode; +extern int GetConfigPath(char *path, size_t size, const char *name); + static inline bool check_path(const char *data, const char *path, string &output) { ostringstream str; diff --git a/UI/platform-x11.cpp b/frontend/utility/platform-x11.cpp similarity index 94% rename from UI/platform-x11.cpp rename to frontend/utility/platform-x11.cpp index 5520e019b..86bd05142 100644 --- a/UI/platform-x11.cpp +++ b/frontend/utility/platform-x11.cpp @@ -16,44 +16,53 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include "obs-app.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include - #include "platform.hpp" -#ifdef __linux__ -#include -#include -#include -#include -#include +#include + +#include +#include +#include + +#include +#include + +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#include +#endif +#include +#include +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#include + +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#include +#include #endif #if defined(__FreeBSD__) || defined(__DragonFly__) #include -#include -#include -#include -#include -#include - -#include -#include -#include #endif +#ifdef __linux__ +#include +#endif +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#ifdef __linux__ +#include +#endif +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#include +using std::ostringstream; using std::string; using std::vector; -using std::ostringstream; #ifdef __linux__ void CheckIfAlreadyRunning(bool &already_running) diff --git a/UI/platform.hpp b/frontend/utility/platform.hpp similarity index 100% rename from UI/platform.hpp rename to frontend/utility/platform.hpp diff --git a/UI/system-info-macos.mm b/frontend/utility/system-info-macos.mm similarity index 100% rename from UI/system-info-macos.mm rename to frontend/utility/system-info-macos.mm diff --git a/UI/system-info-posix.cpp b/frontend/utility/system-info-posix.cpp similarity index 100% rename from UI/system-info-posix.cpp rename to frontend/utility/system-info-posix.cpp diff --git a/UI/system-info-windows.cpp b/frontend/utility/system-info-windows.cpp similarity index 99% rename from UI/system-info-windows.cpp rename to frontend/utility/system-info-windows.cpp index 084cdffe6..d428c06dd 100644 --- a/UI/system-info-windows.cpp +++ b/frontend/utility/system-info-windows.cpp @@ -1,15 +1,16 @@ #include "system-info.hpp" -#include -#include -#include - #include #include #include #include #include +#include + +#include +#include + static std::optional> system_gpu_data() { ComPtr factory; diff --git a/UI/system-info.hpp b/frontend/utility/system-info.hpp similarity index 100% rename from UI/system-info.hpp rename to frontend/utility/system-info.hpp diff --git a/UI/undo-stack-obs.cpp b/frontend/utility/undo_stack.cpp similarity index 97% rename from UI/undo-stack-obs.cpp rename to frontend/utility/undo_stack.cpp index 163a1180d..8ce5d6cc3 100644 --- a/UI/undo-stack-obs.cpp +++ b/frontend/utility/undo_stack.cpp @@ -1,6 +1,8 @@ -#include "moc_undo-stack-obs.cpp" +#include "undo_stack.hpp" -#include +#include + +#include "moc_undo_stack.cpp" #define MAX_STACK_SIZE 5000 diff --git a/UI/undo-stack-obs.hpp b/frontend/utility/undo_stack.hpp similarity index 96% rename from UI/undo-stack-obs.hpp rename to frontend/utility/undo_stack.hpp index 050471279..6486d9922 100644 --- a/UI/undo-stack-obs.hpp +++ b/frontend/utility/undo_stack.hpp @@ -1,15 +1,13 @@ #pragma once +#include "ui_OBSBasic.h" + #include #include #include #include -#include #include -#include - -#include "ui_OBSBasic.h" class undo_stack : public QObject { Q_OBJECT diff --git a/UI/update/update-helpers.cpp b/frontend/utility/update-helpers.cpp similarity index 100% rename from UI/update/update-helpers.cpp rename to frontend/utility/update-helpers.cpp diff --git a/UI/update/update-helpers.hpp b/frontend/utility/update-helpers.hpp similarity index 81% rename from UI/update/update-helpers.hpp rename to frontend/utility/update-helpers.hpp index 14d6e2c97..f07ee4f9a 100644 --- a/UI/update/update-helpers.hpp +++ b/frontend/utility/update-helpers.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include std::string strprintf(const char *format, ...); diff --git a/UI/win-dll-blocklist.c b/frontend/utility/win-dll-blocklist.c similarity index 100% rename from UI/win-dll-blocklist.c rename to frontend/utility/win-dll-blocklist.c From a0668682b76730b33bc14cd66805e3c5bfa284f1 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 25 Nov 2024 16:16:18 +0100 Subject: [PATCH 19/37] frontend: Prepare UI utility files for splits --- .../utility/FFmpegCodec.cpp | 0 .../utility/FFmpegCodec.hpp | 0 frontend/utility/FFmpegFormat.cpp | 219 +++++++++++++ frontend/utility/FFmpegFormat.hpp | 146 +++++++++ frontend/utility/FFmpegShared.hpp | 146 +++++++++ .../utility/MacUpdateThread.cpp | 0 .../utility/MacUpdateThread.hpp | 0 frontend/utility/OBSSparkle.hpp | 51 +++ .../utility/OBSSparkle.mm | 0 frontend/utility/OBSUpdateDelegate.h | 79 +++++ frontend/utility/OBSUpdateDelegate.mm | 79 +++++ .../utility/WhatsNewBrowserInitThread.cpp | 0 .../utility/WhatsNewBrowserInitThread.hpp | 0 frontend/utility/WhatsNewInfoThread.cpp | 295 ++++++++++++++++++ frontend/utility/WhatsNewInfoThread.hpp | 36 +++ 15 files changed, 1051 insertions(+) rename UI/ffmpeg-utils.cpp => frontend/utility/FFmpegCodec.cpp (100%) rename UI/ffmpeg-utils.hpp => frontend/utility/FFmpegCodec.hpp (100%) create mode 100644 frontend/utility/FFmpegFormat.cpp create mode 100644 frontend/utility/FFmpegFormat.hpp create mode 100644 frontend/utility/FFmpegShared.hpp rename UI/update/mac-update.cpp => frontend/utility/MacUpdateThread.cpp (100%) rename UI/update/mac-update.hpp => frontend/utility/MacUpdateThread.hpp (100%) create mode 100644 frontend/utility/OBSSparkle.hpp rename UI/update/sparkle-updater.mm => frontend/utility/OBSSparkle.mm (100%) create mode 100644 frontend/utility/OBSUpdateDelegate.h create mode 100644 frontend/utility/OBSUpdateDelegate.mm rename UI/update/shared-update.cpp => frontend/utility/WhatsNewBrowserInitThread.cpp (100%) rename UI/update/shared-update.hpp => frontend/utility/WhatsNewBrowserInitThread.hpp (100%) create mode 100644 frontend/utility/WhatsNewInfoThread.cpp create mode 100644 frontend/utility/WhatsNewInfoThread.hpp diff --git a/UI/ffmpeg-utils.cpp b/frontend/utility/FFmpegCodec.cpp similarity index 100% rename from UI/ffmpeg-utils.cpp rename to frontend/utility/FFmpegCodec.cpp diff --git a/UI/ffmpeg-utils.hpp b/frontend/utility/FFmpegCodec.hpp similarity index 100% rename from UI/ffmpeg-utils.hpp rename to frontend/utility/FFmpegCodec.hpp diff --git a/frontend/utility/FFmpegFormat.cpp b/frontend/utility/FFmpegFormat.cpp new file mode 100644 index 000000000..88e9b0eab --- /dev/null +++ b/frontend/utility/FFmpegFormat.cpp @@ -0,0 +1,219 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + 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 "ffmpeg-utils.hpp" + +#include +#include + +extern "C" { +#include +} + +using namespace std; + +vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility) +{ + vector codecs; + const AVCodec *codec; + void *i = 0; + + while ((codec = av_codec_iterate(&i)) != nullptr) { + // Not an encoding codec + if (!av_codec_is_encoder(codec)) + continue; + // Skip if not supported and compatibility check not disabled + if (!ignore_compatibility && !av_codec_get_tag(format.codec_tags, codec->id)) { + continue; + } + + codecs.emplace_back(codec); + } + + return codecs; +} + +static bool is_output_device(const AVClass *avclass) +{ + if (!avclass) + return false; + + switch (avclass->category) { + case AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT: + case AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT: + case AV_CLASS_CATEGORY_DEVICE_OUTPUT: + return true; + default: + return false; + } +} + +vector GetSupportedFormats() +{ + vector formats; + const AVOutputFormat *output_format; + + void *i = 0; + while ((output_format = av_muxer_iterate(&i)) != nullptr) { + if (is_output_device(output_format->priv_class)) + continue; + + formats.emplace_back(output_format); + } + + return formats; +} + +FFmpegCodec FFmpegFormat::GetDefaultEncoder(FFmpegCodecType codec_type) const +{ + const AVCodecID codec_id = codec_type == VIDEO ? video_codec : audio_codec; + if (codec_type == UNKNOWN || codec_id == AV_CODEC_ID_NONE) + return {}; + + if (auto codec = avcodec_find_encoder(codec_id)) + return {codec}; + + /* Fall back to using the format name as the encoder, + * this works for some formats such as FLV. */ + return FFmpegCodec{name, codec_id, codec_type}; +} + +bool FFCodecAndFormatCompatible(const char *codec, const char *format) +{ + if (!codec || !format) + return false; + + const AVOutputFormat *output_format = av_guess_format(format, nullptr, nullptr); + if (!output_format) + return false; + + const AVCodecDescriptor *codec_desc = avcodec_descriptor_get_by_name(codec); + if (!codec_desc) + return false; + + return avformat_query_codec(output_format, codec_desc->id, FF_COMPLIANCE_NORMAL) == 1; +} + +static const unordered_set builtin_codecs = { + "h264", "hevc", "av1", "prores", "aac", "opus", "alac", "flac", "pcm_s16le", "pcm_s24le", "pcm_f32le", +}; + +bool IsBuiltinCodec(const char *codec) +{ + return builtin_codecs.count(codec) > 0; +} + +static const unordered_map> codec_compat = { + // Technically our muxer supports HEVC and AV1 as well, but nothing else does + {"flv", + { + "h264", + "aac", + }}, + {"mpegts", + { + "h264", + "hevc", + "aac", + "opus", + }}, + {"hls", + // Also using MPEG-TS in our case, but no Opus support + { + "h264", + "hevc", + "aac", + }}, + {"mp4", + { + "h264", + "hevc", + "av1", + "aac", + "opus", + "alac", + "flac", + "pcm_s16le", + "pcm_s24le", + "pcm_f32le", + }}, + {"fragmented_mp4", + { + "h264", + "hevc", + "av1", + "aac", + "opus", + "alac", + "flac", + "pcm_s16le", + "pcm_s24le", + "pcm_f32le", + }}, + // Not part of FFmpeg, see obs-outputs module + {"hybrid_mp4", + { + "h264", + "hevc", + "av1", + "aac", + "opus", + "alac", + "flac", + "pcm_s16le", + "pcm_s24le", + "pcm_f32le", + }}, + {"mov", + { + "h264", + "hevc", + "prores", + "aac", + "alac", + "pcm_s16le", + "pcm_s24le", + "pcm_f32le", + }}, + {"fragmented_mov", + { + "h264", + "hevc", + "prores", + "aac", + "alac", + "pcm_s16le", + "pcm_s24le", + "pcm_f32le", + }}, + // MKV supports everything + {"mkv", {}}, +}; + +bool ContainerSupportsCodec(const string &container, const string &codec) +{ + auto iter = codec_compat.find(container); + if (iter == codec_compat.end()) + return false; + + auto codecs = iter->second; + // Assume everything is supported + if (codecs.empty()) + return true; + + return codecs.count(codec) > 0; +} diff --git a/frontend/utility/FFmpegFormat.hpp b/frontend/utility/FFmpegFormat.hpp new file mode 100644 index 000000000..4a0ef5eba --- /dev/null +++ b/frontend/utility/FFmpegFormat.hpp @@ -0,0 +1,146 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + 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 +#include +#include + +extern "C" { +#include +#include +} + +enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; + +/* This needs to handle a few special cases due to how the format is used in the UI: + * - strequal(nullptr, "") must be true + * - strequal("", nullptr) must be true + * - strequal(nullptr, nullptr) must be true + */ +static bool strequal(const char *a, const char *b) +{ + if (!a && !b) + return true; + if (!a && *b == 0) + return true; + if (!b && *a == 0) + return true; + if (!a || !b) + return false; + + return strcmp(a, b) == 0; +} + +struct FFmpegCodec; + +struct FFmpegFormat { + const char *name; + const char *long_name; + const char *mime_type; + const char *extensions; + AVCodecID audio_codec; + AVCodecID video_codec; + const AVCodecTag *const *codec_tags; + + FFmpegFormat() = default; + + FFmpegFormat(const char *name, const char *mime_type) + : name(name), + long_name(nullptr), + mime_type(mime_type), + extensions(nullptr), + audio_codec(AV_CODEC_ID_NONE), + video_codec(AV_CODEC_ID_NONE), + codec_tags(nullptr) + { + } + + FFmpegFormat(const AVOutputFormat *av_format) + : name(av_format->name), + long_name(av_format->long_name), + mime_type(av_format->mime_type), + extensions(av_format->extensions), + audio_codec(av_format->audio_codec), + video_codec(av_format->video_codec), + codec_tags(av_format->codec_tag) + { + } + + FFmpegCodec GetDefaultEncoder(FFmpegCodecType codec_type) const; + + bool HasAudio() const { return audio_codec != AV_CODEC_ID_NONE; } + bool HasVideo() const { return video_codec != AV_CODEC_ID_NONE; } + + bool operator==(const FFmpegFormat &format) const + { + if (!strequal(name, format.name)) + return false; + + return strequal(mime_type, format.mime_type); + } +}; +Q_DECLARE_METATYPE(FFmpegFormat) + +struct FFmpegCodec { + const char *name; + const char *long_name; + int id; + + FFmpegCodecType type; + + FFmpegCodec() = default; + + FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) + : name(name), + long_name(nullptr), + id(id), + type(type) + { + } + + FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) + { + switch (codec->type) { + case AVMEDIA_TYPE_AUDIO: + type = AUDIO; + break; + case AVMEDIA_TYPE_VIDEO: + type = VIDEO; + break; + default: + type = UNKNOWN; + } + } + + bool operator==(const FFmpegCodec &codec) const + { + if (id != codec.id) + return false; + + return strequal(name, codec.name); + } +}; +Q_DECLARE_METATYPE(FFmpegCodec) + +std::vector GetSupportedFormats(); +std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); + +bool FFCodecAndFormatCompatible(const char *codec, const char *format); +bool IsBuiltinCodec(const char *codec); +bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/frontend/utility/FFmpegShared.hpp b/frontend/utility/FFmpegShared.hpp new file mode 100644 index 000000000..4a0ef5eba --- /dev/null +++ b/frontend/utility/FFmpegShared.hpp @@ -0,0 +1,146 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + 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 +#include +#include + +extern "C" { +#include +#include +} + +enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; + +/* This needs to handle a few special cases due to how the format is used in the UI: + * - strequal(nullptr, "") must be true + * - strequal("", nullptr) must be true + * - strequal(nullptr, nullptr) must be true + */ +static bool strequal(const char *a, const char *b) +{ + if (!a && !b) + return true; + if (!a && *b == 0) + return true; + if (!b && *a == 0) + return true; + if (!a || !b) + return false; + + return strcmp(a, b) == 0; +} + +struct FFmpegCodec; + +struct FFmpegFormat { + const char *name; + const char *long_name; + const char *mime_type; + const char *extensions; + AVCodecID audio_codec; + AVCodecID video_codec; + const AVCodecTag *const *codec_tags; + + FFmpegFormat() = default; + + FFmpegFormat(const char *name, const char *mime_type) + : name(name), + long_name(nullptr), + mime_type(mime_type), + extensions(nullptr), + audio_codec(AV_CODEC_ID_NONE), + video_codec(AV_CODEC_ID_NONE), + codec_tags(nullptr) + { + } + + FFmpegFormat(const AVOutputFormat *av_format) + : name(av_format->name), + long_name(av_format->long_name), + mime_type(av_format->mime_type), + extensions(av_format->extensions), + audio_codec(av_format->audio_codec), + video_codec(av_format->video_codec), + codec_tags(av_format->codec_tag) + { + } + + FFmpegCodec GetDefaultEncoder(FFmpegCodecType codec_type) const; + + bool HasAudio() const { return audio_codec != AV_CODEC_ID_NONE; } + bool HasVideo() const { return video_codec != AV_CODEC_ID_NONE; } + + bool operator==(const FFmpegFormat &format) const + { + if (!strequal(name, format.name)) + return false; + + return strequal(mime_type, format.mime_type); + } +}; +Q_DECLARE_METATYPE(FFmpegFormat) + +struct FFmpegCodec { + const char *name; + const char *long_name; + int id; + + FFmpegCodecType type; + + FFmpegCodec() = default; + + FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) + : name(name), + long_name(nullptr), + id(id), + type(type) + { + } + + FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) + { + switch (codec->type) { + case AVMEDIA_TYPE_AUDIO: + type = AUDIO; + break; + case AVMEDIA_TYPE_VIDEO: + type = VIDEO; + break; + default: + type = UNKNOWN; + } + } + + bool operator==(const FFmpegCodec &codec) const + { + if (id != codec.id) + return false; + + return strequal(name, codec.name); + } +}; +Q_DECLARE_METATYPE(FFmpegCodec) + +std::vector GetSupportedFormats(); +std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); + +bool FFCodecAndFormatCompatible(const char *codec, const char *format); +bool IsBuiltinCodec(const char *codec); +bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/UI/update/mac-update.cpp b/frontend/utility/MacUpdateThread.cpp similarity index 100% rename from UI/update/mac-update.cpp rename to frontend/utility/MacUpdateThread.cpp diff --git a/UI/update/mac-update.hpp b/frontend/utility/MacUpdateThread.hpp similarity index 100% rename from UI/update/mac-update.hpp rename to frontend/utility/MacUpdateThread.hpp diff --git a/frontend/utility/OBSSparkle.hpp b/frontend/utility/OBSSparkle.hpp new file mode 100644 index 000000000..cedac60eb --- /dev/null +++ b/frontend/utility/OBSSparkle.hpp @@ -0,0 +1,51 @@ +#ifndef MAC_UPDATER_H +#define MAC_UPDATER_H + +#include + +#include +#include +#include + +class QAction; + +class MacUpdateThread : public QThread { + Q_OBJECT + + bool manualUpdate; + + virtual void run() override; + + void info(const QString &title, const QString &text); + +signals: + void Result(const QString &branch, bool manual); + +private slots: + void infoMsg(const QString &title, const QString &text); + +public: + MacUpdateThread(bool manual) : manualUpdate(manual) {} +}; + +#ifdef __OBJC__ +@class OBSUpdateDelegate; +#endif + +class OBSSparkle : public QObject { + Q_OBJECT + +public: + OBSSparkle(const char *branch, QAction *checkForUpdatesAction); + void setBranch(const char *branch); + void checkForUpdates(bool manualCheck); + +private: +#ifdef __OBJC__ + OBSUpdateDelegate *updaterDelegate; +#else + void *updaterDelegate; +#endif +}; + +#endif diff --git a/UI/update/sparkle-updater.mm b/frontend/utility/OBSSparkle.mm similarity index 100% rename from UI/update/sparkle-updater.mm rename to frontend/utility/OBSSparkle.mm diff --git a/frontend/utility/OBSUpdateDelegate.h b/frontend/utility/OBSUpdateDelegate.h new file mode 100644 index 000000000..2afc7e83c --- /dev/null +++ b/frontend/utility/OBSUpdateDelegate.h @@ -0,0 +1,79 @@ +#include "mac-update.hpp" + +#include + +#import +#import + +@interface OBSUpdateDelegate : NSObject { +} +@property (copy) NSString *branch; +@property (nonatomic) SPUStandardUpdaterController *updaterController; +@end + +@implementation OBSUpdateDelegate { +} + +@synthesize branch; + +- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater +{ + return [NSSet setWithObject:branch]; +} + +- (void)observeCanCheckForUpdatesWithAction:(QAction *)action +{ + [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) + options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) + context:(void *) action]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { + QAction *menuAction = (QAction *) context; + menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)dealloc +{ + @autoreleasepool { + [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; + } +} + +@end + +OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) +{ + @autoreleasepool { + updaterDelegate = [[OBSUpdateDelegate alloc] init]; + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; + updaterDelegate.updaterController = + [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate + userDriverDelegate:nil]; + [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; + } +} + +void OBSSparkle::setBranch(const char *branch) +{ + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; +} + +void OBSSparkle::checkForUpdates(bool manualCheck) +{ + @autoreleasepool { + if (manualCheck) { + [updaterDelegate.updaterController checkForUpdates:nil]; + } else { + [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; + } + } +} diff --git a/frontend/utility/OBSUpdateDelegate.mm b/frontend/utility/OBSUpdateDelegate.mm new file mode 100644 index 000000000..2afc7e83c --- /dev/null +++ b/frontend/utility/OBSUpdateDelegate.mm @@ -0,0 +1,79 @@ +#include "mac-update.hpp" + +#include + +#import +#import + +@interface OBSUpdateDelegate : NSObject { +} +@property (copy) NSString *branch; +@property (nonatomic) SPUStandardUpdaterController *updaterController; +@end + +@implementation OBSUpdateDelegate { +} + +@synthesize branch; + +- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater +{ + return [NSSet setWithObject:branch]; +} + +- (void)observeCanCheckForUpdatesWithAction:(QAction *)action +{ + [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) + options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) + context:(void *) action]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { + QAction *menuAction = (QAction *) context; + menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)dealloc +{ + @autoreleasepool { + [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; + } +} + +@end + +OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) +{ + @autoreleasepool { + updaterDelegate = [[OBSUpdateDelegate alloc] init]; + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; + updaterDelegate.updaterController = + [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate + userDriverDelegate:nil]; + [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; + } +} + +void OBSSparkle::setBranch(const char *branch) +{ + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; +} + +void OBSSparkle::checkForUpdates(bool manualCheck) +{ + @autoreleasepool { + if (manualCheck) { + [updaterDelegate.updaterController checkForUpdates:nil]; + } else { + [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; + } + } +} diff --git a/UI/update/shared-update.cpp b/frontend/utility/WhatsNewBrowserInitThread.cpp similarity index 100% rename from UI/update/shared-update.cpp rename to frontend/utility/WhatsNewBrowserInitThread.cpp diff --git a/UI/update/shared-update.hpp b/frontend/utility/WhatsNewBrowserInitThread.hpp similarity index 100% rename from UI/update/shared-update.hpp rename to frontend/utility/WhatsNewBrowserInitThread.hpp diff --git a/frontend/utility/WhatsNewInfoThread.cpp b/frontend/utility/WhatsNewInfoThread.cpp new file mode 100644 index 000000000..0aedd8d95 --- /dev/null +++ b/frontend/utility/WhatsNewInfoThread.cpp @@ -0,0 +1,295 @@ +#include "moc_shared-update.cpp" +#include "crypto-helpers.hpp" +#include "update-helpers.hpp" +#include "obs-app.hpp" +#include "remote-text.hpp" +#include "platform.hpp" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#ifdef BROWSER_AVAILABLE +#include + +struct QCef; +extern QCef *cef; +#endif + +#ifndef MAC_WHATSNEW_URL +#define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + +#ifndef WIN_WHATSNEW_URL +#define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + +#ifndef LINUX_WHATSNEW_URL +#define LINUX_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + +#ifdef __APPLE__ +#define WHATSNEW_URL MAC_WHATSNEW_URL +#elif defined(_WIN32) +#define WHATSNEW_URL WIN_WHATSNEW_URL +#else +#define WHATSNEW_URL LINUX_WHATSNEW_URL +#endif + +#define HASH_READ_BUF_SIZE 65536 +#define BLAKE2_HASH_LENGTH 20 + +/* ------------------------------------------------------------------------ */ + +static bool QuickWriteFile(const char *file, const std::string &data) +try { + std::ofstream fileStream(std::filesystem::u8path(file), std::ios::binary); + if (fileStream.fail()) + throw strprintf("Failed to open file '%s': %s", file, strerror(errno)); + + fileStream.write(data.data(), data.size()); + if (fileStream.fail()) + throw strprintf("Failed to write file '%s': %s", file, strerror(errno)); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static bool QuickReadFile(const char *file, std::string &data) +try { + std::ifstream fileStream(std::filesystem::u8path(file), std::ios::binary); + if (!fileStream.is_open() || fileStream.fail()) + throw strprintf("Failed to open file '%s': %s", file, strerror(errno)); + + fileStream.seekg(0, fileStream.end); + size_t size = fileStream.tellg(); + fileStream.seekg(0); + + data.resize(size); + fileStream.read(&data[0], size); + + if (fileStream.fail()) + throw strprintf("Failed to write file '%s': %s", file, strerror(errno)); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static bool CalculateFileHash(const char *path, uint8_t *hash) +try { + blake2b_state blake2; + if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0) + return false; + + std::ifstream file(std::filesystem::u8path(path), std::ios::binary); + if (!file.is_open() || file.fail()) + return false; + + char buf[HASH_READ_BUF_SIZE]; + + for (;;) { + file.read(buf, HASH_READ_BUF_SIZE); + size_t read = file.gcount(); + if (blake2b_update(&blake2, &buf, read) != 0) + return false; + if (file.eof()) + break; + } + + if (blake2b_final(&blake2, hash, BLAKE2_HASH_LENGTH) != 0) + return false; + + return true; + +} catch (std::string &text) { + blog(LOG_DEBUG, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +void GenerateGUID(std::string &guid) +{ + const char alphabet[] = "0123456789abcdef"; + QRandomGenerator *rng = QRandomGenerator::system(); + + guid.resize(40); + + for (size_t i = 0; i < 40; i++) { + guid[i] = alphabet[rng->bounded(0, 16)]; + } +} + +std::string GetProgramGUID() +{ + static std::mutex m; + std::lock_guard lock(m); + + /* NOTE: this is an arbitrary random number that we use to count the + * number of unique OBS installations and is not associated with any + * kind of identifiable information */ + const char *pguid = config_get_string(App()->GetAppConfig(), "General", "InstallGUID"); + std::string guid; + if (pguid) + guid = pguid; + + if (guid.empty()) { + GenerateGUID(guid); + + if (!guid.empty()) + config_set_string(App()->GetAppConfig(), "General", "InstallGUID", guid.c_str()); + } + + return guid; +} + +/* ------------------------------------------------------------------------ */ + +static void LoadPublicKey(std::string &pubkey) +{ + std::string pemFilePath; + + if (!GetDataFilePath("OBSPublicRSAKey.pem", pemFilePath)) + throw std::string("Could not find OBS public key file!"); + if (!QuickReadFile(pemFilePath.c_str(), pubkey)) + throw std::string("Could not read OBS public key file!"); +} + +static bool CheckDataSignature(const char *name, const std::string &data, const std::string &hexSig) +try { + static std::mutex pubkey_mutex; + static std::string obsPubKey; + + if (hexSig.empty() || hexSig.length() > 0xFFFF || (hexSig.length() & 1) != 0) + throw strprintf("Missing or invalid signature for %s: %s", name, hexSig.c_str()); + + std::scoped_lock lock(pubkey_mutex); + if (obsPubKey.empty()) + LoadPublicKey(obsPubKey); + + // Convert hex string to bytes + auto signature = QByteArray::fromHex(hexSig.data()); + + if (!VerifySignature((uint8_t *)obsPubKey.data(), obsPubKey.size(), (uint8_t *)data.data(), data.size(), + (uint8_t *)signature.data(), signature.size())) + throw strprintf("Signature check failed for %s", name); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, + const std::vector &extraHeaders) +{ + long responseCode; + std::vector headers; + std::string error; + std::string signature; + std::string data; + uint8_t fileHash[BLAKE2_HASH_LENGTH]; + bool success; + + BPtr filePath = GetAppConfigPathPtr(file); + + if (!extraHeaders.empty()) { + headers.insert(headers.end(), extraHeaders.begin(), extraHeaders.end()); + } + + /* ----------------------------------- * + * avoid downloading file again */ + + if (CalculateFileHash(filePath, fileHash)) { + auto hash = QByteArray::fromRawData((const char *)fileHash, BLAKE2_HASH_LENGTH); + + QString header = "If-None-Match: " + hash.toHex(); + headers.push_back(header.toStdString()); + } + + /* ----------------------------------- * + * get current install GUID */ + + std::string guid = GetProgramGUID(); + + if (!guid.empty()) { + std::string header = "X-OBS2-GUID: " + guid; + headers.push_back(std::move(header)); + } + + /* ----------------------------------- * + * get file from server */ + + success = GetRemoteFile(url, data, error, &responseCode, nullptr, "", nullptr, headers, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return false; + + throw strprintf("Failed to fetch %s file: %s", name, error.c_str()); + } + + /* ----------------------------------- * + * verify file signature */ + + if (responseCode == 200) { + success = CheckDataSignature(name, data, signature); + if (!success) + throw strprintf("Invalid %s signature", name); + } + + /* ----------------------------------- * + * write or load file */ + + if (responseCode == 200) { + if (!QuickWriteFile(filePath, data)) + throw strprintf("Could not write file '%s'", filePath.Get()); + } else if (out) { /* Only read file if caller wants data */ + if (!QuickReadFile(filePath, data)) + throw strprintf("Could not read file '%s'", filePath.Get()); + } + + if (out) + *out = data; + + /* ----------------------------------- * + * success */ + return true; +} + +void WhatsNewInfoThread::run() +try { + std::string text; + + if (FetchAndVerifyFile("whatsnew", "obs-studio/updates/whatsnew.json", WHATSNEW_URL, &text)) { + emit Result(QString::fromStdString(text)); + } +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} + +/* ------------------------------------------------------------------------ */ + +void WhatsNewBrowserInitThread::run() +{ +#ifdef BROWSER_AVAILABLE + cef->wait_for_browser_init(); +#endif + emit Result(url); +} diff --git a/frontend/utility/WhatsNewInfoThread.hpp b/frontend/utility/WhatsNewInfoThread.hpp new file mode 100644 index 000000000..c33b8823d --- /dev/null +++ b/frontend/utility/WhatsNewInfoThread.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include +#include + +bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, + const std::vector &extraHeaders = std::vector()); + +class WhatsNewInfoThread : public QThread { + Q_OBJECT + + virtual void run() override; + +signals: + void Result(const QString &text); + +public: + inline WhatsNewInfoThread() {} +}; + +class WhatsNewBrowserInitThread : public QThread { + Q_OBJECT + + QString url; + + virtual void run() override; + +signals: + void Result(const QString &url); + +public: + inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} +}; From a1ba3247c6a46909cdeac764f2eb017be7976d06 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 4 Dec 2024 19:37:52 +0100 Subject: [PATCH 20/37] frontend: Split UI utility implementation into single files per class --- frontend/utility/FFmpegCodec.cpp | 52 +--- frontend/utility/FFmpegCodec.hpp | 77 +---- frontend/utility/FFmpegFormat.cpp | 157 +--------- frontend/utility/FFmpegFormat.hpp | 73 +---- frontend/utility/FFmpegShared.hpp | 107 +------ frontend/utility/MacUpdateThread.cpp | 15 +- frontend/utility/MacUpdateThread.hpp | 31 +- frontend/utility/OBSSparkle.hpp | 31 +- frontend/utility/OBSSparkle.mm | 53 +--- frontend/utility/OBSUpdateDelegate.h | 98 ++---- frontend/utility/OBSUpdateDelegate.mm | 43 +-- .../utility/WhatsNewBrowserInitThread.cpp | 285 +----------------- .../utility/WhatsNewBrowserInitThread.hpp | 19 -- frontend/utility/WhatsNewInfoThread.cpp | 38 +-- frontend/utility/WhatsNewInfoThread.hpp | 18 -- 15 files changed, 64 insertions(+), 1033 deletions(-) diff --git a/frontend/utility/FFmpegCodec.cpp b/frontend/utility/FFmpegCodec.cpp index 88e9b0eab..b93edb56b 100644 --- a/frontend/utility/FFmpegCodec.cpp +++ b/frontend/utility/FFmpegCodec.cpp @@ -15,15 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "ffmpeg-utils.hpp" +#include "FFmpegCodec.hpp" +#include "FFmpegFormat.hpp" #include #include -extern "C" { -#include -} - using namespace std; vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility) @@ -47,51 +44,6 @@ vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_comp return codecs; } -static bool is_output_device(const AVClass *avclass) -{ - if (!avclass) - return false; - - switch (avclass->category) { - case AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT: - case AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT: - case AV_CLASS_CATEGORY_DEVICE_OUTPUT: - return true; - default: - return false; - } -} - -vector GetSupportedFormats() -{ - vector formats; - const AVOutputFormat *output_format; - - void *i = 0; - while ((output_format = av_muxer_iterate(&i)) != nullptr) { - if (is_output_device(output_format->priv_class)) - continue; - - formats.emplace_back(output_format); - } - - return formats; -} - -FFmpegCodec FFmpegFormat::GetDefaultEncoder(FFmpegCodecType codec_type) const -{ - const AVCodecID codec_id = codec_type == VIDEO ? video_codec : audio_codec; - if (codec_type == UNKNOWN || codec_id == AV_CODEC_ID_NONE) - return {}; - - if (auto codec = avcodec_find_encoder(codec_id)) - return {codec}; - - /* Fall back to using the format name as the encoder, - * this works for some formats such as FLV. */ - return FFmpegCodec{name, codec_id, codec_type}; -} - bool FFCodecAndFormatCompatible(const char *codec, const char *format) { if (!codec || !format) diff --git a/frontend/utility/FFmpegCodec.hpp b/frontend/utility/FFmpegCodec.hpp index 4a0ef5eba..27893fd8e 100644 --- a/frontend/utility/FFmpegCodec.hpp +++ b/frontend/utility/FFmpegCodec.hpp @@ -17,85 +17,15 @@ #pragma once -#include -#include -#include +#include "FFmpegShared.hpp" extern "C" { #include #include } +#include -enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; - -/* This needs to handle a few special cases due to how the format is used in the UI: - * - strequal(nullptr, "") must be true - * - strequal("", nullptr) must be true - * - strequal(nullptr, nullptr) must be true - */ -static bool strequal(const char *a, const char *b) -{ - if (!a && !b) - return true; - if (!a && *b == 0) - return true; - if (!b && *a == 0) - return true; - if (!a || !b) - return false; - - return strcmp(a, b) == 0; -} - -struct FFmpegCodec; - -struct FFmpegFormat { - const char *name; - const char *long_name; - const char *mime_type; - const char *extensions; - AVCodecID audio_codec; - AVCodecID video_codec; - const AVCodecTag *const *codec_tags; - - FFmpegFormat() = default; - - FFmpegFormat(const char *name, const char *mime_type) - : name(name), - long_name(nullptr), - mime_type(mime_type), - extensions(nullptr), - audio_codec(AV_CODEC_ID_NONE), - video_codec(AV_CODEC_ID_NONE), - codec_tags(nullptr) - { - } - - FFmpegFormat(const AVOutputFormat *av_format) - : name(av_format->name), - long_name(av_format->long_name), - mime_type(av_format->mime_type), - extensions(av_format->extensions), - audio_codec(av_format->audio_codec), - video_codec(av_format->video_codec), - codec_tags(av_format->codec_tag) - { - } - - FFmpegCodec GetDefaultEncoder(FFmpegCodecType codec_type) const; - - bool HasAudio() const { return audio_codec != AV_CODEC_ID_NONE; } - bool HasVideo() const { return video_codec != AV_CODEC_ID_NONE; } - - bool operator==(const FFmpegFormat &format) const - { - if (!strequal(name, format.name)) - return false; - - return strequal(mime_type, format.mime_type); - } -}; -Q_DECLARE_METATYPE(FFmpegFormat) +struct FFmpegFormat; struct FFmpegCodec { const char *name; @@ -138,7 +68,6 @@ struct FFmpegCodec { }; Q_DECLARE_METATYPE(FFmpegCodec) -std::vector GetSupportedFormats(); std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); bool FFCodecAndFormatCompatible(const char *codec, const char *format); diff --git a/frontend/utility/FFmpegFormat.cpp b/frontend/utility/FFmpegFormat.cpp index 88e9b0eab..8d8c4e606 100644 --- a/frontend/utility/FFmpegFormat.cpp +++ b/frontend/utility/FFmpegFormat.cpp @@ -15,38 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include "ffmpeg-utils.hpp" - -#include -#include - -extern "C" { -#include -} +#include "FFmpegFormat.hpp" +#include "FFmpegCodec.hpp" using namespace std; -vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility) -{ - vector codecs; - const AVCodec *codec; - void *i = 0; - - while ((codec = av_codec_iterate(&i)) != nullptr) { - // Not an encoding codec - if (!av_codec_is_encoder(codec)) - continue; - // Skip if not supported and compatibility check not disabled - if (!ignore_compatibility && !av_codec_get_tag(format.codec_tags, codec->id)) { - continue; - } - - codecs.emplace_back(codec); - } - - return codecs; -} - static bool is_output_device(const AVClass *avclass) { if (!avclass) @@ -91,129 +64,3 @@ FFmpegCodec FFmpegFormat::GetDefaultEncoder(FFmpegCodecType codec_type) const * this works for some formats such as FLV. */ return FFmpegCodec{name, codec_id, codec_type}; } - -bool FFCodecAndFormatCompatible(const char *codec, const char *format) -{ - if (!codec || !format) - return false; - - const AVOutputFormat *output_format = av_guess_format(format, nullptr, nullptr); - if (!output_format) - return false; - - const AVCodecDescriptor *codec_desc = avcodec_descriptor_get_by_name(codec); - if (!codec_desc) - return false; - - return avformat_query_codec(output_format, codec_desc->id, FF_COMPLIANCE_NORMAL) == 1; -} - -static const unordered_set builtin_codecs = { - "h264", "hevc", "av1", "prores", "aac", "opus", "alac", "flac", "pcm_s16le", "pcm_s24le", "pcm_f32le", -}; - -bool IsBuiltinCodec(const char *codec) -{ - return builtin_codecs.count(codec) > 0; -} - -static const unordered_map> codec_compat = { - // Technically our muxer supports HEVC and AV1 as well, but nothing else does - {"flv", - { - "h264", - "aac", - }}, - {"mpegts", - { - "h264", - "hevc", - "aac", - "opus", - }}, - {"hls", - // Also using MPEG-TS in our case, but no Opus support - { - "h264", - "hevc", - "aac", - }}, - {"mp4", - { - "h264", - "hevc", - "av1", - "aac", - "opus", - "alac", - "flac", - "pcm_s16le", - "pcm_s24le", - "pcm_f32le", - }}, - {"fragmented_mp4", - { - "h264", - "hevc", - "av1", - "aac", - "opus", - "alac", - "flac", - "pcm_s16le", - "pcm_s24le", - "pcm_f32le", - }}, - // Not part of FFmpeg, see obs-outputs module - {"hybrid_mp4", - { - "h264", - "hevc", - "av1", - "aac", - "opus", - "alac", - "flac", - "pcm_s16le", - "pcm_s24le", - "pcm_f32le", - }}, - {"mov", - { - "h264", - "hevc", - "prores", - "aac", - "alac", - "pcm_s16le", - "pcm_s24le", - "pcm_f32le", - }}, - {"fragmented_mov", - { - "h264", - "hevc", - "prores", - "aac", - "alac", - "pcm_s16le", - "pcm_s24le", - "pcm_f32le", - }}, - // MKV supports everything - {"mkv", {}}, -}; - -bool ContainerSupportsCodec(const string &container, const string &codec) -{ - auto iter = codec_compat.find(container); - if (iter == codec_compat.end()) - return false; - - auto codecs = iter->second; - // Assume everything is supported - if (codecs.empty()) - return true; - - return codecs.count(codec) > 0; -} diff --git a/frontend/utility/FFmpegFormat.hpp b/frontend/utility/FFmpegFormat.hpp index 4a0ef5eba..6c44bcd59 100644 --- a/frontend/utility/FFmpegFormat.hpp +++ b/frontend/utility/FFmpegFormat.hpp @@ -17,35 +17,13 @@ #pragma once -#include -#include -#include +#include "FFmpegShared.hpp" extern "C" { #include #include } - -enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; - -/* This needs to handle a few special cases due to how the format is used in the UI: - * - strequal(nullptr, "") must be true - * - strequal("", nullptr) must be true - * - strequal(nullptr, nullptr) must be true - */ -static bool strequal(const char *a, const char *b) -{ - if (!a && !b) - return true; - if (!a && *b == 0) - return true; - if (!b && *a == 0) - return true; - if (!a || !b) - return false; - - return strcmp(a, b) == 0; -} +#include struct FFmpegCodec; @@ -95,52 +73,7 @@ struct FFmpegFormat { return strequal(mime_type, format.mime_type); } }; + Q_DECLARE_METATYPE(FFmpegFormat) -struct FFmpegCodec { - const char *name; - const char *long_name; - int id; - - FFmpegCodecType type; - - FFmpegCodec() = default; - - FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) - : name(name), - long_name(nullptr), - id(id), - type(type) - { - } - - FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) - { - switch (codec->type) { - case AVMEDIA_TYPE_AUDIO: - type = AUDIO; - break; - case AVMEDIA_TYPE_VIDEO: - type = VIDEO; - break; - default: - type = UNKNOWN; - } - } - - bool operator==(const FFmpegCodec &codec) const - { - if (id != codec.id) - return false; - - return strequal(name, codec.name); - } -}; -Q_DECLARE_METATYPE(FFmpegCodec) - std::vector GetSupportedFormats(); -std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); - -bool FFCodecAndFormatCompatible(const char *codec, const char *format); -bool IsBuiltinCodec(const char *codec); -bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/frontend/utility/FFmpegShared.hpp b/frontend/utility/FFmpegShared.hpp index 4a0ef5eba..c0a3a573e 100644 --- a/frontend/utility/FFmpegShared.hpp +++ b/frontend/utility/FFmpegShared.hpp @@ -17,14 +17,7 @@ #pragma once -#include -#include -#include - -extern "C" { -#include -#include -} +#include enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; @@ -46,101 +39,3 @@ static bool strequal(const char *a, const char *b) return strcmp(a, b) == 0; } - -struct FFmpegCodec; - -struct FFmpegFormat { - const char *name; - const char *long_name; - const char *mime_type; - const char *extensions; - AVCodecID audio_codec; - AVCodecID video_codec; - const AVCodecTag *const *codec_tags; - - FFmpegFormat() = default; - - FFmpegFormat(const char *name, const char *mime_type) - : name(name), - long_name(nullptr), - mime_type(mime_type), - extensions(nullptr), - audio_codec(AV_CODEC_ID_NONE), - video_codec(AV_CODEC_ID_NONE), - codec_tags(nullptr) - { - } - - FFmpegFormat(const AVOutputFormat *av_format) - : name(av_format->name), - long_name(av_format->long_name), - mime_type(av_format->mime_type), - extensions(av_format->extensions), - audio_codec(av_format->audio_codec), - video_codec(av_format->video_codec), - codec_tags(av_format->codec_tag) - { - } - - FFmpegCodec GetDefaultEncoder(FFmpegCodecType codec_type) const; - - bool HasAudio() const { return audio_codec != AV_CODEC_ID_NONE; } - bool HasVideo() const { return video_codec != AV_CODEC_ID_NONE; } - - bool operator==(const FFmpegFormat &format) const - { - if (!strequal(name, format.name)) - return false; - - return strequal(mime_type, format.mime_type); - } -}; -Q_DECLARE_METATYPE(FFmpegFormat) - -struct FFmpegCodec { - const char *name; - const char *long_name; - int id; - - FFmpegCodecType type; - - FFmpegCodec() = default; - - FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) - : name(name), - long_name(nullptr), - id(id), - type(type) - { - } - - FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) - { - switch (codec->type) { - case AVMEDIA_TYPE_AUDIO: - type = AUDIO; - break; - case AVMEDIA_TYPE_VIDEO: - type = VIDEO; - break; - default: - type = UNKNOWN; - } - } - - bool operator==(const FFmpegCodec &codec) const - { - if (id != codec.id) - return false; - - return strequal(name, codec.name); - } -}; -Q_DECLARE_METATYPE(FFmpegCodec) - -std::vector GetSupportedFormats(); -std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); - -bool FFCodecAndFormatCompatible(const char *codec, const char *format); -bool IsBuiltinCodec(const char *codec); -bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/frontend/utility/MacUpdateThread.cpp b/frontend/utility/MacUpdateThread.cpp index 33fca9430..ffaa3881d 100644 --- a/frontend/utility/MacUpdateThread.cpp +++ b/frontend/utility/MacUpdateThread.cpp @@ -1,20 +1,15 @@ -#include "update-helpers.hpp" -#include "shared-update.hpp" -#include "moc_mac-update.cpp" -#include "obs-app.hpp" +#include "MacUpdateThread.hpp" -#include +#include +#include #include -#include -/* ------------------------------------------------------------------------ */ +#include "moc_MacUpdateThread.cpp" static const char *MAC_BRANCHES_URL = "https://obsproject.com/update_studio/branches.json"; static const char *MAC_DEFAULT_BRANCH = "stable"; -/* ------------------------------------------------------------------------ */ - bool GetBranch(std::string &selectedBranch) { const char *config_branch = config_get_string(App()->GetAppConfig(), "General", "UpdateBranch"); @@ -39,8 +34,6 @@ bool GetBranch(std::string &selectedBranch) return found; } -/* ------------------------------------------------------------------------ */ - void MacUpdateThread::infoMsg(const QString &title, const QString &text) { OBSMessageBox::information(App()->GetMainWindow(), title, text); diff --git a/frontend/utility/MacUpdateThread.hpp b/frontend/utility/MacUpdateThread.hpp index cedac60eb..c4bddbd10 100644 --- a/frontend/utility/MacUpdateThread.hpp +++ b/frontend/utility/MacUpdateThread.hpp @@ -1,13 +1,6 @@ -#ifndef MAC_UPDATER_H -#define MAC_UPDATER_H - -#include +#pragma once #include -#include -#include - -class QAction; class MacUpdateThread : public QThread { Q_OBJECT @@ -27,25 +20,3 @@ private slots: public: MacUpdateThread(bool manual) : manualUpdate(manual) {} }; - -#ifdef __OBJC__ -@class OBSUpdateDelegate; -#endif - -class OBSSparkle : public QObject { - Q_OBJECT - -public: - OBSSparkle(const char *branch, QAction *checkForUpdatesAction); - void setBranch(const char *branch); - void checkForUpdates(bool manualCheck); - -private: -#ifdef __OBJC__ - OBSUpdateDelegate *updaterDelegate; -#else - void *updaterDelegate; -#endif -}; - -#endif diff --git a/frontend/utility/OBSSparkle.hpp b/frontend/utility/OBSSparkle.hpp index cedac60eb..3f4cffc3c 100644 --- a/frontend/utility/OBSSparkle.hpp +++ b/frontend/utility/OBSSparkle.hpp @@ -1,33 +1,8 @@ -#ifndef MAC_UPDATER_H -#define MAC_UPDATER_H +#pragma once -#include - -#include -#include -#include +#import class QAction; - -class MacUpdateThread : public QThread { - Q_OBJECT - - bool manualUpdate; - - virtual void run() override; - - void info(const QString &title, const QString &text); - -signals: - void Result(const QString &branch, bool manual); - -private slots: - void infoMsg(const QString &title, const QString &text); - -public: - MacUpdateThread(bool manual) : manualUpdate(manual) {} -}; - #ifdef __OBJC__ @class OBSUpdateDelegate; #endif @@ -47,5 +22,3 @@ private: void *updaterDelegate; #endif }; - -#endif diff --git a/frontend/utility/OBSSparkle.mm b/frontend/utility/OBSSparkle.mm index 2afc7e83c..bfb1911aa 100644 --- a/frontend/utility/OBSSparkle.mm +++ b/frontend/utility/OBSSparkle.mm @@ -1,54 +1,5 @@ -#include "mac-update.hpp" - -#include - -#import -#import - -@interface OBSUpdateDelegate : NSObject { -} -@property (copy) NSString *branch; -@property (nonatomic) SPUStandardUpdaterController *updaterController; -@end - -@implementation OBSUpdateDelegate { -} - -@synthesize branch; - -- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater -{ - return [NSSet setWithObject:branch]; -} - -- (void)observeCanCheckForUpdatesWithAction:(QAction *)action -{ - [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) - options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) - context:(void *) action]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context -{ - if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { - QAction *menuAction = (QAction *) context; - menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); - } else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -- (void)dealloc -{ - @autoreleasepool { - [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; - } -} - -@end +#import "OBSSparkle.hpp" +#import "OBSUpdateDelegate.h" OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) { diff --git a/frontend/utility/OBSUpdateDelegate.h b/frontend/utility/OBSUpdateDelegate.h index 2afc7e83c..1e6f68306 100644 --- a/frontend/utility/OBSUpdateDelegate.h +++ b/frontend/utility/OBSUpdateDelegate.h @@ -1,79 +1,37 @@ -#include "mac-update.hpp" +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer + + 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 +#pragma once -#import +#import #import +#import + @interface OBSUpdateDelegate : NSObject { } -@property (copy) NSString *branch; -@property (nonatomic) SPUStandardUpdaterController *updaterController; -@end +@property (copy) NSString *_Nonnull branch; +@property (nonatomic) SPUStandardUpdaterController *_Nonnull updaterController; -@implementation OBSUpdateDelegate { -} - -@synthesize branch; - -- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater -{ - return [NSSet setWithObject:branch]; -} - -- (void)observeCanCheckForUpdatesWithAction:(QAction *)action -{ - [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) - options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) - context:(void *) action]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context -{ - if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { - QAction *menuAction = (QAction *) context; - menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); - } else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -- (void)dealloc -{ - @autoreleasepool { - [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; - } -} +- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater; +- (void)observeCanCheckForUpdatesWithAction:(nonnull QAction *)action; +- (void)observeValueForKeyPath:(NSString *_Nullable)keyPath + ofObject:(id _Nullable)object + change:(NSDictionary *_Nullable)change + context:(void *_Nullable)context; @end - -OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) -{ - @autoreleasepool { - updaterDelegate = [[OBSUpdateDelegate alloc] init]; - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; - updaterDelegate.updaterController = - [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate - userDriverDelegate:nil]; - [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; - } -} - -void OBSSparkle::setBranch(const char *branch) -{ - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; -} - -void OBSSparkle::checkForUpdates(bool manualCheck) -{ - @autoreleasepool { - if (manualCheck) { - [updaterDelegate.updaterController checkForUpdates:nil]; - } else { - [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; - } - } -} diff --git a/frontend/utility/OBSUpdateDelegate.mm b/frontend/utility/OBSUpdateDelegate.mm index 2afc7e83c..734091e8b 100644 --- a/frontend/utility/OBSUpdateDelegate.mm +++ b/frontend/utility/OBSUpdateDelegate.mm @@ -1,15 +1,4 @@ -#include "mac-update.hpp" - -#include - -#import -#import - -@interface OBSUpdateDelegate : NSObject { -} -@property (copy) NSString *branch; -@property (nonatomic) SPUStandardUpdaterController *updaterController; -@end +#import "OBSUpdateDelegate.h" @implementation OBSUpdateDelegate { } @@ -21,7 +10,7 @@ return [NSSet setWithObject:branch]; } -- (void)observeCanCheckForUpdatesWithAction:(QAction *)action +- (void)observeCanCheckForUpdatesWithAction:(nonnull QAction *)action; { [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) @@ -49,31 +38,3 @@ } @end - -OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) -{ - @autoreleasepool { - updaterDelegate = [[OBSUpdateDelegate alloc] init]; - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; - updaterDelegate.updaterController = - [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate - userDriverDelegate:nil]; - [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; - } -} - -void OBSSparkle::setBranch(const char *branch) -{ - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; -} - -void OBSSparkle::checkForUpdates(bool manualCheck) -{ - @autoreleasepool { - if (manualCheck) { - [updaterDelegate.updaterController checkForUpdates:nil]; - } else { - [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; - } - } -} diff --git a/frontend/utility/WhatsNewBrowserInitThread.cpp b/frontend/utility/WhatsNewBrowserInitThread.cpp index 0aedd8d95..200cf285f 100644 --- a/frontend/utility/WhatsNewBrowserInitThread.cpp +++ b/frontend/utility/WhatsNewBrowserInitThread.cpp @@ -1,291 +1,16 @@ -#include "moc_shared-update.cpp" -#include "crypto-helpers.hpp" -#include "update-helpers.hpp" -#include "obs-app.hpp" -#include "remote-text.hpp" -#include "platform.hpp" - -#include -#include - -#include -#include -#include - -#include -#include -#include +#include "WhatsNewBrowserInitThread.hpp" #ifdef BROWSER_AVAILABLE #include +#endif +#include "moc_WhatsNewBrowserInitThread.cpp" + +#ifdef BROWSER_AVAILABLE struct QCef; extern QCef *cef; #endif -#ifndef MAC_WHATSNEW_URL -#define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" -#endif - -#ifndef WIN_WHATSNEW_URL -#define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" -#endif - -#ifndef LINUX_WHATSNEW_URL -#define LINUX_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" -#endif - -#ifdef __APPLE__ -#define WHATSNEW_URL MAC_WHATSNEW_URL -#elif defined(_WIN32) -#define WHATSNEW_URL WIN_WHATSNEW_URL -#else -#define WHATSNEW_URL LINUX_WHATSNEW_URL -#endif - -#define HASH_READ_BUF_SIZE 65536 -#define BLAKE2_HASH_LENGTH 20 - -/* ------------------------------------------------------------------------ */ - -static bool QuickWriteFile(const char *file, const std::string &data) -try { - std::ofstream fileStream(std::filesystem::u8path(file), std::ios::binary); - if (fileStream.fail()) - throw strprintf("Failed to open file '%s': %s", file, strerror(errno)); - - fileStream.write(data.data(), data.size()); - if (fileStream.fail()) - throw strprintf("Failed to write file '%s': %s", file, strerror(errno)); - - return true; - -} catch (std::string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -static bool QuickReadFile(const char *file, std::string &data) -try { - std::ifstream fileStream(std::filesystem::u8path(file), std::ios::binary); - if (!fileStream.is_open() || fileStream.fail()) - throw strprintf("Failed to open file '%s': %s", file, strerror(errno)); - - fileStream.seekg(0, fileStream.end); - size_t size = fileStream.tellg(); - fileStream.seekg(0); - - data.resize(size); - fileStream.read(&data[0], size); - - if (fileStream.fail()) - throw strprintf("Failed to write file '%s': %s", file, strerror(errno)); - - return true; - -} catch (std::string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -static bool CalculateFileHash(const char *path, uint8_t *hash) -try { - blake2b_state blake2; - if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0) - return false; - - std::ifstream file(std::filesystem::u8path(path), std::ios::binary); - if (!file.is_open() || file.fail()) - return false; - - char buf[HASH_READ_BUF_SIZE]; - - for (;;) { - file.read(buf, HASH_READ_BUF_SIZE); - size_t read = file.gcount(); - if (blake2b_update(&blake2, &buf, read) != 0) - return false; - if (file.eof()) - break; - } - - if (blake2b_final(&blake2, hash, BLAKE2_HASH_LENGTH) != 0) - return false; - - return true; - -} catch (std::string &text) { - blog(LOG_DEBUG, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -/* ------------------------------------------------------------------------ */ - -void GenerateGUID(std::string &guid) -{ - const char alphabet[] = "0123456789abcdef"; - QRandomGenerator *rng = QRandomGenerator::system(); - - guid.resize(40); - - for (size_t i = 0; i < 40; i++) { - guid[i] = alphabet[rng->bounded(0, 16)]; - } -} - -std::string GetProgramGUID() -{ - static std::mutex m; - std::lock_guard lock(m); - - /* NOTE: this is an arbitrary random number that we use to count the - * number of unique OBS installations and is not associated with any - * kind of identifiable information */ - const char *pguid = config_get_string(App()->GetAppConfig(), "General", "InstallGUID"); - std::string guid; - if (pguid) - guid = pguid; - - if (guid.empty()) { - GenerateGUID(guid); - - if (!guid.empty()) - config_set_string(App()->GetAppConfig(), "General", "InstallGUID", guid.c_str()); - } - - return guid; -} - -/* ------------------------------------------------------------------------ */ - -static void LoadPublicKey(std::string &pubkey) -{ - std::string pemFilePath; - - if (!GetDataFilePath("OBSPublicRSAKey.pem", pemFilePath)) - throw std::string("Could not find OBS public key file!"); - if (!QuickReadFile(pemFilePath.c_str(), pubkey)) - throw std::string("Could not read OBS public key file!"); -} - -static bool CheckDataSignature(const char *name, const std::string &data, const std::string &hexSig) -try { - static std::mutex pubkey_mutex; - static std::string obsPubKey; - - if (hexSig.empty() || hexSig.length() > 0xFFFF || (hexSig.length() & 1) != 0) - throw strprintf("Missing or invalid signature for %s: %s", name, hexSig.c_str()); - - std::scoped_lock lock(pubkey_mutex); - if (obsPubKey.empty()) - LoadPublicKey(obsPubKey); - - // Convert hex string to bytes - auto signature = QByteArray::fromHex(hexSig.data()); - - if (!VerifySignature((uint8_t *)obsPubKey.data(), obsPubKey.size(), (uint8_t *)data.data(), data.size(), - (uint8_t *)signature.data(), signature.size())) - throw strprintf("Signature check failed for %s", name); - - return true; - -} catch (std::string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -/* ------------------------------------------------------------------------ */ - -bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, - const std::vector &extraHeaders) -{ - long responseCode; - std::vector headers; - std::string error; - std::string signature; - std::string data; - uint8_t fileHash[BLAKE2_HASH_LENGTH]; - bool success; - - BPtr filePath = GetAppConfigPathPtr(file); - - if (!extraHeaders.empty()) { - headers.insert(headers.end(), extraHeaders.begin(), extraHeaders.end()); - } - - /* ----------------------------------- * - * avoid downloading file again */ - - if (CalculateFileHash(filePath, fileHash)) { - auto hash = QByteArray::fromRawData((const char *)fileHash, BLAKE2_HASH_LENGTH); - - QString header = "If-None-Match: " + hash.toHex(); - headers.push_back(header.toStdString()); - } - - /* ----------------------------------- * - * get current install GUID */ - - std::string guid = GetProgramGUID(); - - if (!guid.empty()) { - std::string header = "X-OBS2-GUID: " + guid; - headers.push_back(std::move(header)); - } - - /* ----------------------------------- * - * get file from server */ - - success = GetRemoteFile(url, data, error, &responseCode, nullptr, "", nullptr, headers, &signature); - - if (!success || (responseCode != 200 && responseCode != 304)) { - if (responseCode == 404) - return false; - - throw strprintf("Failed to fetch %s file: %s", name, error.c_str()); - } - - /* ----------------------------------- * - * verify file signature */ - - if (responseCode == 200) { - success = CheckDataSignature(name, data, signature); - if (!success) - throw strprintf("Invalid %s signature", name); - } - - /* ----------------------------------- * - * write or load file */ - - if (responseCode == 200) { - if (!QuickWriteFile(filePath, data)) - throw strprintf("Could not write file '%s'", filePath.Get()); - } else if (out) { /* Only read file if caller wants data */ - if (!QuickReadFile(filePath, data)) - throw strprintf("Could not read file '%s'", filePath.Get()); - } - - if (out) - *out = data; - - /* ----------------------------------- * - * success */ - return true; -} - -void WhatsNewInfoThread::run() -try { - std::string text; - - if (FetchAndVerifyFile("whatsnew", "obs-studio/updates/whatsnew.json", WHATSNEW_URL, &text)) { - emit Result(QString::fromStdString(text)); - } -} catch (std::string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); -} - -/* ------------------------------------------------------------------------ */ - void WhatsNewBrowserInitThread::run() { #ifdef BROWSER_AVAILABLE diff --git a/frontend/utility/WhatsNewBrowserInitThread.hpp b/frontend/utility/WhatsNewBrowserInitThread.hpp index c33b8823d..6fd28acb7 100644 --- a/frontend/utility/WhatsNewBrowserInitThread.hpp +++ b/frontend/utility/WhatsNewBrowserInitThread.hpp @@ -1,25 +1,6 @@ #pragma once #include -#include - -#include -#include - -bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, - const std::vector &extraHeaders = std::vector()); - -class WhatsNewInfoThread : public QThread { - Q_OBJECT - - virtual void run() override; - -signals: - void Result(const QString &text); - -public: - inline WhatsNewInfoThread() {} -}; class WhatsNewBrowserInitThread : public QThread { Q_OBJECT diff --git a/frontend/utility/WhatsNewInfoThread.cpp b/frontend/utility/WhatsNewInfoThread.cpp index 0aedd8d95..24ebd010f 100644 --- a/frontend/utility/WhatsNewInfoThread.cpp +++ b/frontend/utility/WhatsNewInfoThread.cpp @@ -1,27 +1,17 @@ -#include "moc_shared-update.cpp" -#include "crypto-helpers.hpp" -#include "update-helpers.hpp" -#include "obs-app.hpp" -#include "remote-text.hpp" -#include "platform.hpp" +#include "WhatsNewInfoThread.hpp" -#include -#include - -#include -#include -#include +#include +#include +#include +#include +#include #include -#include -#include +#include -#ifdef BROWSER_AVAILABLE -#include +#include -struct QCef; -extern QCef *cef; -#endif +#include "moc_WhatsNewInfoThread.cpp" #ifndef MAC_WHATSNEW_URL #define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" @@ -283,13 +273,3 @@ try { } catch (std::string &text) { blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); } - -/* ------------------------------------------------------------------------ */ - -void WhatsNewBrowserInitThread::run() -{ -#ifdef BROWSER_AVAILABLE - cef->wait_for_browser_init(); -#endif - emit Result(url); -} diff --git a/frontend/utility/WhatsNewInfoThread.hpp b/frontend/utility/WhatsNewInfoThread.hpp index c33b8823d..d384eeb58 100644 --- a/frontend/utility/WhatsNewInfoThread.hpp +++ b/frontend/utility/WhatsNewInfoThread.hpp @@ -1,10 +1,6 @@ #pragma once #include -#include - -#include -#include bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, const std::vector &extraHeaders = std::vector()); @@ -20,17 +16,3 @@ signals: public: inline WhatsNewInfoThread() {} }; - -class WhatsNewBrowserInitThread : public QThread { - Q_OBJECT - - QString url; - - virtual void run() override; - -signals: - void Result(const QString &url); - -public: - inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} -}; From f813121bb9f538d60a5872ac0d4dc2d3de79f58e Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 2 Dec 2024 21:33:44 +0100 Subject: [PATCH 21/37] frontend: Add renamed Qt UI Widgets --- .../widgets/OBSBasicControls.cpp | 5 ++-- .../widgets/OBSBasicControls.hpp | 6 ++--- .../widgets/OBSBasicPreview.cpp | 17 ++++--------- .../widgets/OBSBasicPreview.hpp | 15 ++++-------- .../widgets/OBSBasicStats.cpp | 15 +++++------- .../widgets/OBSBasicStats.hpp | 13 +++++----- .../widgets/OBSMainWindow.hpp | 6 ++--- .../widgets/OBSProjector.cpp | 24 ++++++++++--------- .../widgets/OBSProjector.hpp | 8 +++---- 9 files changed, 45 insertions(+), 64 deletions(-) rename UI/basic-controls.cpp => frontend/widgets/OBSBasicControls.cpp (99%) rename UI/basic-controls.hpp => frontend/widgets/OBSBasicControls.hpp (100%) rename UI/window-basic-preview.cpp => frontend/widgets/OBSBasicPreview.cpp (99%) rename UI/window-basic-preview.hpp => frontend/widgets/OBSBasicPreview.hpp (96%) rename UI/window-basic-stats.cpp => frontend/widgets/OBSBasicStats.cpp (98%) rename UI/window-basic-stats.hpp => frontend/widgets/OBSBasicStats.hpp (95%) rename UI/window-main.hpp => frontend/widgets/OBSMainWindow.hpp (76%) rename UI/window-projector.cpp => frontend/widgets/OBSProjector.cpp (97%) rename UI/window-projector.hpp => frontend/widgets/OBSProjector.hpp (94%) diff --git a/UI/basic-controls.cpp b/frontend/widgets/OBSBasicControls.cpp similarity index 99% rename from UI/basic-controls.cpp rename to frontend/widgets/OBSBasicControls.cpp index fe9483136..7fcc1bc06 100644 --- a/UI/basic-controls.cpp +++ b/frontend/widgets/OBSBasicControls.cpp @@ -1,6 +1,7 @@ -#include "moc_basic-controls.cpp" +#include "OBSBasicControls.hpp" +#include "OBSBasic.hpp" -#include "window-basic-main.hpp" +#include "moc_OBSBasicControls.cpp" OBSBasicControls::OBSBasicControls(OBSBasic *main) : QFrame(nullptr), ui(new Ui::OBSBasicControls) { diff --git a/UI/basic-controls.hpp b/frontend/widgets/OBSBasicControls.hpp similarity index 100% rename from UI/basic-controls.hpp rename to frontend/widgets/OBSBasicControls.hpp index 6aa677802..a79c43b47 100644 --- a/UI/basic-controls.hpp +++ b/frontend/widgets/OBSBasicControls.hpp @@ -1,14 +1,14 @@ #pragma once -#include +#include "ui_OBSBasicControls.h" #include #include #include -class OBSBasic; +#include -#include "ui_OBSBasicControls.h" +class OBSBasic; class OBSBasicControls : public QFrame { Q_OBJECT diff --git a/UI/window-basic-preview.cpp b/frontend/widgets/OBSBasicPreview.cpp similarity index 99% rename from UI/window-basic-preview.cpp rename to frontend/widgets/OBSBasicPreview.cpp index 361e318c2..2e4464ee6 100644 --- a/UI/window-basic-preview.cpp +++ b/frontend/widgets/OBSBasicPreview.cpp @@ -1,16 +1,9 @@ -#include -#include +#include "OBSBasicPreview.hpp" -#include -#include -#include -#include -#include -#include "moc_window-basic-preview.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "display-helpers.hpp" +#include +#include + +#include "moc_OBSBasicPreview.cpp" #define HANDLE_RADIUS 4.0f #define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f) diff --git a/UI/window-basic-preview.hpp b/frontend/widgets/OBSBasicPreview.hpp similarity index 96% rename from UI/window-basic-preview.hpp rename to frontend/widgets/OBSBasicPreview.hpp index e880a8dac..67d7db296 100644 --- a/UI/window-basic-preview.hpp +++ b/frontend/widgets/OBSBasicPreview.hpp @@ -1,17 +1,10 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include "qt-display.hpp" -#include "obs-app.hpp" -#include "preview-controls.hpp" +#include "OBSQTDisplay.hpp" -class OBSBasic; -class QMouseEvent; +#include + +#include #define ITEM_LEFT (1 << 0) #define ITEM_RIGHT (1 << 1) diff --git a/UI/window-basic-stats.cpp b/frontend/widgets/OBSBasicStats.cpp similarity index 98% rename from UI/window-basic-stats.cpp rename to frontend/widgets/OBSBasicStats.cpp index bb8ab0bd8..f4a224f37 100644 --- a/UI/window-basic-stats.cpp +++ b/frontend/widgets/OBSBasicStats.cpp @@ -1,19 +1,16 @@ -#include "obs-frontend-api/obs-frontend-api.h" +#include "OBSBasicStats.hpp" -#include "moc_window-basic-stats.cpp" -#include "window-basic-main.hpp" -#include "platform.hpp" -#include "obs-app.hpp" +#include #include + +#include +#include #include #include #include -#include -#include -#include -#include +#include "moc_OBSBasicStats.cpp" #define TIMER_INTERVAL 2000 #define REC_TIME_LEFT_INTERVAL 30000 diff --git a/UI/window-basic-stats.hpp b/frontend/widgets/OBSBasicStats.hpp similarity index 95% rename from UI/window-basic-stats.hpp rename to frontend/widgets/OBSBasicStats.hpp index c1d434bf6..350b5c866 100644 --- a/UI/window-basic-stats.hpp +++ b/frontend/widgets/OBSBasicStats.hpp @@ -1,16 +1,15 @@ #pragma once +#include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include + +class QLabel; class QGridLayout; -class QCloseEvent; class OBSBasicStats : public QFrame { Q_OBJECT diff --git a/UI/window-main.hpp b/frontend/widgets/OBSMainWindow.hpp similarity index 76% rename from UI/window-main.hpp rename to frontend/widgets/OBSMainWindow.hpp index cfecfc375..ad726ac34 100644 --- a/UI/window-main.hpp +++ b/frontend/widgets/OBSMainWindow.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include +#include + class OBSMainWindow : public QMainWindow { Q_OBJECT @@ -12,6 +12,4 @@ public: virtual config_t *Config() const = 0; virtual void OBSInit() = 0; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const = 0; }; diff --git a/UI/window-projector.cpp b/frontend/widgets/OBSProjector.cpp similarity index 97% rename from UI/window-projector.cpp rename to frontend/widgets/OBSProjector.cpp index 60f873bc9..7d6beb0d8 100644 --- a/UI/window-projector.cpp +++ b/frontend/widgets/OBSProjector.cpp @@ -1,15 +1,17 @@ -#include -#include -#include -#include -#include +#include "OBSProjector.hpp" + +#include +#include +#include +#include +#include + #include -#include "moc_window-projector.cpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" -#include "platform.hpp" -#include "multiview.hpp" + +#include +#include + +#include "moc_OBSProjector.cpp" static QList multiviewProjectors; diff --git a/UI/window-projector.hpp b/frontend/widgets/OBSProjector.hpp similarity index 94% rename from UI/window-projector.hpp rename to frontend/widgets/OBSProjector.hpp index f95063261..417192ee0 100644 --- a/UI/window-projector.hpp +++ b/frontend/widgets/OBSProjector.hpp @@ -1,8 +1,8 @@ #pragma once -#include -#include "qt-display.hpp" -#include "multiview.hpp" +#include "OBSQTDisplay.hpp" + +class Multiview; enum class ProjectorType { Source, @@ -12,8 +12,6 @@ enum class ProjectorType { Multiview, }; -class QMouseEvent; - class OBSProjector : public OBSQTDisplay { Q_OBJECT From 9f887c76d38a2285637b87a7bc2da75859dee8c3 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 03:44:30 +0100 Subject: [PATCH 22/37] frontend: Prepare Qt UI Widgets for splits --- .../components/VolumeSlider.cpp | 0 .../components/VolumeSlider.hpp | 0 .../utility/AdvancedOutput.cpp | 0 frontend/utility/AdvancedOutput.hpp | 2541 ++++ frontend/utility/BasicOutputHandler.cpp | 2541 ++++ .../utility/BasicOutputHandler.hpp | 0 .../utility/QuickTransition.cpp | 0 .../utility/QuickTransition.hpp | 0 .../utility/SceneRenameDelegate.cpp | 0 frontend/utility/SceneRenameDelegate.hpp | 1382 +++ .../utility/ScreenshotObj.cpp | 0 frontend/utility/SimpleOutput.cpp | 2541 ++++ frontend/utility/SimpleOutput.hpp | 2541 ++++ .../StartMultiTrackVideoStreamingGuard.hpp | 2541 ++++ .../utility/SurfaceEventFilter.hpp | 0 frontend/utility/VolumeMeterTimer.cpp | 1507 +++ frontend/utility/VolumeMeterTimer.hpp | 341 + frontend/widgets/ColorSelect.cpp | 10203 ++++++++++++++++ frontend/widgets/ColorSelect.hpp | 1382 +++ frontend/widgets/OBSBasic.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic.hpp | 1382 +++ .../widgets/OBSBasicStatusBar.cpp | 0 .../widgets/OBSBasicStatusBar.hpp | 0 .../widgets/OBSBasic_Browser.cpp | 0 frontend/widgets/OBSBasic_Clipboard.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_ContextToolbar.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Docks.cpp | 10203 ++++++++++++++++ .../widgets/OBSBasic_Dropfiles.cpp | 0 frontend/widgets/OBSBasic_Hotkeys.cpp | 10203 ++++++++++++++++ .../widgets/OBSBasic_Icons.cpp | 0 frontend/widgets/OBSBasic_MainControls.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_OutputHandler.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Preview.cpp | 10203 ++++++++++++++++ .../widgets/OBSBasic_Profiles.cpp | 0 frontend/widgets/OBSBasic_Projectors.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Recording.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_ReplayBuffer.cpp | 10203 ++++++++++++++++ .../widgets/OBSBasic_SceneCollections.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_SceneItems.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Scenes.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Screenshots.cpp | 347 + frontend/widgets/OBSBasic_Service.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_StatusBar.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Streaming.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_StudioMode.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_SysTray.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Transitions.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_Updater.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_VirtualCam.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_VolControl.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSBasic_YouTube.cpp | 10203 ++++++++++++++++ frontend/widgets/OBSQTDisplay.cpp | 243 + .../widgets/OBSQTDisplay.hpp | 0 frontend/widgets/StatusBarWidget.cpp | 601 + frontend/widgets/StatusBarWidget.hpp | 119 + frontend/widgets/VolControl.cpp | 1507 +++ frontend/widgets/VolControl.hpp | 341 + .../widgets/VolumeAccessibleInterface.cpp | 1507 +++ .../widgets/VolumeAccessibleInterface.hpp | 341 + frontend/widgets/VolumeMeter.cpp | 1507 +++ frontend/widgets/VolumeMeter.hpp | 341 + 61 files changed, 280628 insertions(+) rename UI/volume-control.cpp => frontend/components/VolumeSlider.cpp (100%) rename UI/volume-control.hpp => frontend/components/VolumeSlider.hpp (100%) rename UI/window-basic-main-outputs.cpp => frontend/utility/AdvancedOutput.cpp (100%) create mode 100644 frontend/utility/AdvancedOutput.hpp create mode 100644 frontend/utility/BasicOutputHandler.cpp rename UI/window-basic-main-outputs.hpp => frontend/utility/BasicOutputHandler.hpp (100%) rename UI/window-basic-main-transitions.cpp => frontend/utility/QuickTransition.cpp (100%) rename UI/window-basic-main.hpp => frontend/utility/QuickTransition.hpp (100%) rename UI/window-basic-main.cpp => frontend/utility/SceneRenameDelegate.cpp (100%) create mode 100644 frontend/utility/SceneRenameDelegate.hpp rename UI/window-basic-main-screenshot.cpp => frontend/utility/ScreenshotObj.cpp (100%) create mode 100644 frontend/utility/SimpleOutput.cpp create mode 100644 frontend/utility/SimpleOutput.hpp create mode 100644 frontend/utility/StartMultiTrackVideoStreamingGuard.hpp rename UI/qt-display.cpp => frontend/utility/SurfaceEventFilter.hpp (100%) create mode 100644 frontend/utility/VolumeMeterTimer.cpp create mode 100644 frontend/utility/VolumeMeterTimer.hpp create mode 100644 frontend/widgets/ColorSelect.cpp create mode 100644 frontend/widgets/ColorSelect.hpp create mode 100644 frontend/widgets/OBSBasic.cpp create mode 100644 frontend/widgets/OBSBasic.hpp rename UI/window-basic-status-bar.cpp => frontend/widgets/OBSBasicStatusBar.cpp (100%) rename UI/window-basic-status-bar.hpp => frontend/widgets/OBSBasicStatusBar.hpp (100%) rename UI/window-basic-main-browser.cpp => frontend/widgets/OBSBasic_Browser.cpp (100%) create mode 100644 frontend/widgets/OBSBasic_Clipboard.cpp create mode 100644 frontend/widgets/OBSBasic_ContextToolbar.cpp create mode 100644 frontend/widgets/OBSBasic_Docks.cpp rename UI/window-basic-main-dropfiles.cpp => frontend/widgets/OBSBasic_Dropfiles.cpp (100%) create mode 100644 frontend/widgets/OBSBasic_Hotkeys.cpp rename UI/window-basic-main-icons.cpp => frontend/widgets/OBSBasic_Icons.cpp (100%) create mode 100644 frontend/widgets/OBSBasic_MainControls.cpp create mode 100644 frontend/widgets/OBSBasic_OutputHandler.cpp create mode 100644 frontend/widgets/OBSBasic_Preview.cpp rename UI/window-basic-main-profiles.cpp => frontend/widgets/OBSBasic_Profiles.cpp (100%) create mode 100644 frontend/widgets/OBSBasic_Projectors.cpp create mode 100644 frontend/widgets/OBSBasic_Recording.cpp create mode 100644 frontend/widgets/OBSBasic_ReplayBuffer.cpp create mode 100644 frontend/widgets/OBSBasic_SceneCollections.cpp create mode 100644 frontend/widgets/OBSBasic_SceneItems.cpp create mode 100644 frontend/widgets/OBSBasic_Scenes.cpp create mode 100644 frontend/widgets/OBSBasic_Screenshots.cpp create mode 100644 frontend/widgets/OBSBasic_Service.cpp create mode 100644 frontend/widgets/OBSBasic_StatusBar.cpp create mode 100644 frontend/widgets/OBSBasic_Streaming.cpp create mode 100644 frontend/widgets/OBSBasic_StudioMode.cpp create mode 100644 frontend/widgets/OBSBasic_SysTray.cpp create mode 100644 frontend/widgets/OBSBasic_Transitions.cpp create mode 100644 frontend/widgets/OBSBasic_Updater.cpp create mode 100644 frontend/widgets/OBSBasic_VirtualCam.cpp create mode 100644 frontend/widgets/OBSBasic_VolControl.cpp create mode 100644 frontend/widgets/OBSBasic_YouTube.cpp create mode 100644 frontend/widgets/OBSQTDisplay.cpp rename UI/qt-display.hpp => frontend/widgets/OBSQTDisplay.hpp (100%) create mode 100644 frontend/widgets/StatusBarWidget.cpp create mode 100644 frontend/widgets/StatusBarWidget.hpp create mode 100644 frontend/widgets/VolControl.cpp create mode 100644 frontend/widgets/VolControl.hpp create mode 100644 frontend/widgets/VolumeAccessibleInterface.cpp create mode 100644 frontend/widgets/VolumeAccessibleInterface.hpp create mode 100644 frontend/widgets/VolumeMeter.cpp create mode 100644 frontend/widgets/VolumeMeter.hpp diff --git a/UI/volume-control.cpp b/frontend/components/VolumeSlider.cpp similarity index 100% rename from UI/volume-control.cpp rename to frontend/components/VolumeSlider.cpp diff --git a/UI/volume-control.hpp b/frontend/components/VolumeSlider.hpp similarity index 100% rename from UI/volume-control.hpp rename to frontend/components/VolumeSlider.hpp diff --git a/UI/window-basic-main-outputs.cpp b/frontend/utility/AdvancedOutput.cpp similarity index 100% rename from UI/window-basic-main-outputs.cpp rename to frontend/utility/AdvancedOutput.cpp diff --git a/frontend/utility/AdvancedOutput.hpp b/frontend/utility/AdvancedOutput.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/AdvancedOutput.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +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) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/BasicOutputHandler.cpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +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) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/UI/window-basic-main-outputs.hpp b/frontend/utility/BasicOutputHandler.hpp similarity index 100% rename from UI/window-basic-main-outputs.hpp rename to frontend/utility/BasicOutputHandler.hpp diff --git a/UI/window-basic-main-transitions.cpp b/frontend/utility/QuickTransition.cpp similarity index 100% rename from UI/window-basic-main-transitions.cpp rename to frontend/utility/QuickTransition.cpp diff --git a/UI/window-basic-main.hpp b/frontend/utility/QuickTransition.hpp similarity index 100% rename from UI/window-basic-main.hpp rename to frontend/utility/QuickTransition.hpp diff --git a/UI/window-basic-main.cpp b/frontend/utility/SceneRenameDelegate.cpp similarity index 100% rename from UI/window-basic-main.cpp rename to frontend/utility/SceneRenameDelegate.cpp diff --git a/frontend/utility/SceneRenameDelegate.hpp b/frontend/utility/SceneRenameDelegate.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/utility/SceneRenameDelegate.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#define SIMPLE_ENCODER_X264 "x264" +#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" +#define SIMPLE_ENCODER_QSV "qsv" +#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" +#define SIMPLE_ENCODER_NVENC "nvenc" +#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" +#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" +#define SIMPLE_ENCODER_AMD "amd" +#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" +#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" +#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" +#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/UI/window-basic-main-screenshot.cpp b/frontend/utility/ScreenshotObj.cpp similarity index 100% rename from UI/window-basic-main-screenshot.cpp rename to frontend/utility/ScreenshotObj.cpp diff --git a/frontend/utility/SimpleOutput.cpp b/frontend/utility/SimpleOutput.cpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/SimpleOutput.cpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +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) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/SimpleOutput.hpp b/frontend/utility/SimpleOutput.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/SimpleOutput.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +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) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +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) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.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), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "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"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + 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); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/UI/qt-display.cpp b/frontend/utility/SurfaceEventFilter.hpp similarity index 100% rename from UI/qt-display.cpp rename to frontend/utility/SurfaceEventFilter.hpp diff --git a/frontend/utility/VolumeMeterTimer.cpp b/frontend/utility/VolumeMeterTimer.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/utility/VolumeMeterTimer.hpp b/frontend/utility/VolumeMeterTimer.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/ColorSelect.cpp b/frontend/widgets/ColorSelect.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/ColorSelect.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/ColorSelect.hpp b/frontend/widgets/ColorSelect.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/widgets/ColorSelect.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#define SIMPLE_ENCODER_X264 "x264" +#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" +#define SIMPLE_ENCODER_QSV "qsv" +#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" +#define SIMPLE_ENCODER_NVENC "nvenc" +#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" +#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" +#define SIMPLE_ENCODER_AMD "amd" +#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" +#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" +#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" +#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/widgets/OBSBasic.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#define SIMPLE_ENCODER_X264 "x264" +#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" +#define SIMPLE_ENCODER_QSV "qsv" +#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" +#define SIMPLE_ENCODER_NVENC "nvenc" +#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" +#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" +#define SIMPLE_ENCODER_AMD "amd" +#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" +#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" +#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" +#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/UI/window-basic-status-bar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp similarity index 100% rename from UI/window-basic-status-bar.cpp rename to frontend/widgets/OBSBasicStatusBar.cpp diff --git a/UI/window-basic-status-bar.hpp b/frontend/widgets/OBSBasicStatusBar.hpp similarity index 100% rename from UI/window-basic-status-bar.hpp rename to frontend/widgets/OBSBasicStatusBar.hpp diff --git a/UI/window-basic-main-browser.cpp b/frontend/widgets/OBSBasic_Browser.cpp similarity index 100% rename from UI/window-basic-main-browser.cpp rename to frontend/widgets/OBSBasic_Browser.cpp diff --git a/frontend/widgets/OBSBasic_Clipboard.cpp b/frontend/widgets/OBSBasic_Clipboard.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Clipboard.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_ContextToolbar.cpp b/frontend/widgets/OBSBasic_ContextToolbar.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_ContextToolbar.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Docks.cpp b/frontend/widgets/OBSBasic_Docks.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Docks.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp similarity index 100% rename from UI/window-basic-main-dropfiles.cpp rename to frontend/widgets/OBSBasic_Dropfiles.cpp diff --git a/frontend/widgets/OBSBasic_Hotkeys.cpp b/frontend/widgets/OBSBasic_Hotkeys.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Hotkeys.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-icons.cpp b/frontend/widgets/OBSBasic_Icons.cpp similarity index 100% rename from UI/window-basic-main-icons.cpp rename to frontend/widgets/OBSBasic_Icons.cpp diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_OutputHandler.cpp b/frontend/widgets/OBSBasic_OutputHandler.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_OutputHandler.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Preview.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp similarity index 100% rename from UI/window-basic-main-profiles.cpp rename to frontend/widgets/OBSBasic_Profiles.cpp diff --git a/frontend/widgets/OBSBasic_Projectors.cpp b/frontend/widgets/OBSBasic_Projectors.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Projectors.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Recording.cpp b/frontend/widgets/OBSBasic_Recording.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Recording.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_ReplayBuffer.cpp b/frontend/widgets/OBSBasic_ReplayBuffer.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_ReplayBuffer.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Scenes.cpp b/frontend/widgets/OBSBasic_Scenes.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Scenes.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Screenshots.cpp b/frontend/widgets/OBSBasic_Screenshots.cpp new file mode 100644 index 000000000..a0b3622d1 --- /dev/null +++ b/frontend/widgets/OBSBasic_Screenshots.cpp @@ -0,0 +1,347 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 "window-basic-main.hpp" +#include "screenshot-obj.hpp" + +#include + +#ifdef _WIN32 +#include +#include +#include +#pragma comment(lib, "windowscodecs.lib") +#endif + +static void ScreenshotTick(void *param, float); + +/* ========================================================================= */ + +ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source)) +{ + obs_add_tick_callback(ScreenshotTick, this); +} + +ScreenshotObj::~ScreenshotObj() +{ + obs_enter_graphics(); + gs_stagesurface_destroy(stagesurf); + gs_texrender_destroy(texrender); + obs_leave_graphics(); + + obs_remove_tick_callback(ScreenshotTick, this); + + if (th.joinable()) { + th.join(); + + if (cx && cy) { + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage( + QTStr("Basic.StatusBar.ScreenshotSavedTo").arg(QT_UTF8(path.c_str()))); + + main->lastScreenshot = path; + + main->OnEvent(OBS_FRONTEND_EVENT_SCREENSHOT_TAKEN); + } + } +} + +void ScreenshotObj::Screenshot() +{ + OBSSource source = OBSGetStrongRef(weakSource); + + if (source) { + cx = obs_source_get_width(source); + cy = obs_source_get_height(source); + } else { + obs_video_info ovi; + obs_get_video_info(&ovi); + cx = ovi.base_width; + cy = ovi.base_height; + } + + if (!cx || !cy) { + blog(LOG_WARNING, "Cannot screenshot, invalid target size"); + obs_remove_tick_callback(ScreenshotTick, this); + deleteLater(); + return; + } + +#ifdef _WIN32 + enum gs_color_space space = obs_source_get_color_space(source, 0, nullptr); + if (space == GS_CS_709_EXTENDED) { + /* Convert for JXR */ + space = GS_CS_709_SCRGB; + } +#else + /* Tonemap to SDR if HDR */ + const enum gs_color_space space = GS_CS_SRGB; +#endif + const enum gs_color_format format = gs_get_format_from_space(space); + + texrender = gs_texrender_create(format, GS_ZS_NONE); + stagesurf = gs_stagesurface_create(cx, cy, format); + + if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) { + vec4 zero; + vec4_zero(&zero); + + gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0); + gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f); + + gs_blend_state_push(); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + if (source) { + obs_source_inc_showing(source); + obs_source_video_render(source); + obs_source_dec_showing(source); + } else { + obs_render_main_texture(); + } + + gs_blend_state_pop(); + gs_texrender_end(texrender); + } +} + +void ScreenshotObj::Download() +{ + gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender)); +} + +void ScreenshotObj::Copy() +{ + uint8_t *videoData = nullptr; + uint32_t videoLinesize = 0; + + if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) { + if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) { + const uint32_t linesize = cx * 8; + half_bytes.reserve(cx * cy * 8); + + for (uint32_t y = 0; y < cy; y++) { + const uint8_t *const line = videoData + (y * videoLinesize); + half_bytes.insert(half_bytes.end(), line, line + linesize); + } + } else { + image = QImage(cx, cy, QImage::Format::Format_RGBX8888); + + int linesize = image.bytesPerLine(); + for (int y = 0; y < (int)cy; y++) + memcpy(image.scanLine(y), videoData + (y * videoLinesize), linesize); + } + + gs_stagesurface_unmap(stagesurf); + } +} + +void ScreenshotObj::Save() +{ + OBSBasic *main = OBSBasic::Get(); + config_t *config = main->Config(); + + const char *mode = config_get_string(config, "Output", "Mode"); + const char *type = config_get_string(config, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") ? config_get_string(config, "AdvOut", "FFFilePath") + : config_get_string(config, "AdvOut", "RecFilePath"); + const char *rec_path = strcmp(mode, "Advanced") ? config_get_string(config, "SimpleOutput", "FilePath") + : adv_path; + + bool noSpace = config_get_bool(config, "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(config, "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(config, "Output", "OverwriteIfExists"); + + const char *ext = half_bytes.empty() ? "png" : "jxr"; + path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists, + GetFormatString(filenameFormat, "Screenshot", nullptr).c_str()); + + th = std::thread([this] { MuxAndFinish(); }); +} + +#ifdef _WIN32 +static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy, IWICBitmapFrameEncode *frameEncode, + IPropertyBag2 *options) +{ + wchar_t lossless[] = L"Lossless"; + PROPBAG2 bag = {}; + bag.pstrName = lossless; + VARIANT value = {}; + value.vt = VT_BOOL; + value.bVal = TRUE; + HRESULT hr = options->Write(1, &bag, &value); + if (FAILED(hr)) + return hr; + + hr = frameEncode->Initialize(options); + if (FAILED(hr)) + return hr; + + hr = frameEncode->SetSize(cx, cy); + if (FAILED(hr)) + return hr; + + hr = frameEncode->SetResolution(72, 72); + if (FAILED(hr)) + return hr; + + WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf; + hr = frameEncode->SetPixelFormat(&pixelFormat); + if (FAILED(hr)) + return hr; + + if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) + return E_FAIL; + + hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels); + if (FAILED(hr)) + return hr; + + hr = frameEncode->Commit(); + if (FAILED(hr)) + return hr; + + return S_OK; +} + +static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy) +{ + Microsoft::WRL::ComPtr factory; + HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr stream; + hr = factory->CreateStream(stream.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = stream->InitializeFromFilename(path, GENERIC_WRITE); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr encoder = NULL; + hr = factory->CreateEncoder(GUID_ContainerFormatWmp, NULL, encoder.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr frameEncode; + Microsoft::WRL::ComPtr options; + hr = encoder->CreateNewFrame(frameEncode.GetAddressOf(), options.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = SaveJxrImage(path, pixels, cx, cy, frameEncode.Get(), options.Get()); + if (FAILED(hr)) + return hr; + + encoder->Commit(); + return S_OK; +} +#endif // #ifdef _WIN32 + +void ScreenshotObj::MuxAndFinish() +{ + if (half_bytes.empty()) { + image.save(QT_UTF8(path.c_str())); + blog(LOG_INFO, "Saved screenshot to '%s'", path.c_str()); + } else { +#ifdef _WIN32 + wchar_t *path_w = nullptr; + os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w); + if (path_w) { + SaveJxr(path_w, half_bytes.data(), cx, cy); + bfree(path_w); + } +#endif // #ifdef _WIN32 + } + + deleteLater(); +} + +/* ========================================================================= */ + +#define STAGE_SCREENSHOT 0 +#define STAGE_DOWNLOAD 1 +#define STAGE_COPY_AND_SAVE 2 +#define STAGE_FINISH 3 + +static void ScreenshotTick(void *param, float) +{ + ScreenshotObj *data = reinterpret_cast(param); + + if (data->stage == STAGE_FINISH) { + return; + } + + obs_enter_graphics(); + + switch (data->stage) { + case STAGE_SCREENSHOT: + data->Screenshot(); + break; + case STAGE_DOWNLOAD: + data->Download(); + break; + case STAGE_COPY_AND_SAVE: + data->Copy(); + QMetaObject::invokeMethod(data, "Save"); + obs_remove_tick_callback(ScreenshotTick, data); + break; + } + + obs_leave_graphics(); + + data->stage++; +} + +void OBSBasic::Screenshot(OBSSource source) +{ + if (!!screenshotData) { + blog(LOG_WARNING, "Cannot take new screenshot, " + "screenshot currently in progress"); + return; + } + + screenshotData = new ScreenshotObj(source); +} + +void OBSBasic::ScreenshotSelectedSource() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (item) { + Screenshot(obs_sceneitem_get_source(item)); + } else { + blog(LOG_INFO, "Could not take a source screenshot: " + "no source selected"); + } +} + +void OBSBasic::ScreenshotProgram() +{ + Screenshot(GetProgramSource()); +} + +void OBSBasic::ScreenshotScene() +{ + Screenshot(GetCurrentSceneSource()); +} diff --git a/frontend/widgets/OBSBasic_Service.cpp b/frontend/widgets/OBSBasic_Service.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Service.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_StatusBar.cpp b/frontend/widgets/OBSBasic_StatusBar.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_StatusBar.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Streaming.cpp b/frontend/widgets/OBSBasic_Streaming.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Streaming.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_StudioMode.cpp b/frontend/widgets/OBSBasic_StudioMode.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_StudioMode.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SysTray.cpp b/frontend/widgets/OBSBasic_SysTray.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SysTray.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Updater.cpp b/frontend/widgets/OBSBasic_Updater.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Updater.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_VirtualCam.cpp b/frontend/widgets/OBSBasic_VirtualCam.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_VirtualCam.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_VolControl.cpp b/frontend/widgets/OBSBasic_VolControl.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_VolControl.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_YouTube.cpp b/frontend/widgets/OBSBasic_YouTube.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_YouTube.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + 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.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * 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; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* 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(); + 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; + } + +#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(); + return; + } +#endif + + 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 (button == QMessageBox::No) { + event->ignore(); + restart = false; + 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); + + 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; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings 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::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); + if (ret <= 0) + return ret; + + if (!*file) + return snprintf(path, size, "%s/%s", profiles_path, profile); + + return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSQTDisplay.cpp b/frontend/widgets/OBSQTDisplay.cpp new file mode 100644 index 000000000..f7c37ae9a --- /dev/null +++ b/frontend/widgets/OBSQTDisplay.cpp @@ -0,0 +1,243 @@ +#include "moc_qt-display.cpp" +#include "display-helpers.hpp" +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#endif + +#ifdef ENABLE_WAYLAND +#include +#endif + +class SurfaceEventFilter : public QObject { + OBSQTDisplay *display; + +public: + SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) override + { + bool result = QObject::eventFilter(obj, event); + QPlatformSurfaceEvent *surfaceEvent; + + switch (event->type()) { + case QEvent::PlatformSurface: + surfaceEvent = static_cast(event); + + switch (surfaceEvent->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + display->DestroyDisplay(); + break; + default: + break; + } + break; + default: + break; + } + + return result; + } +}; + +static inline long long color_to_int(const QColor &color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +static inline QColor rgba_to_color(uint32_t rgba) +{ + return QColor::fromRgb(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF, (rgba >> 24) & 0xFF); +} + +static bool QTToGSWindow(QWindow *window, gs_window &gswindow) +{ + bool success = true; + +#ifdef _WIN32 + gswindow.hwnd = (HWND)window->winId(); +#elif __APPLE__ + gswindow.view = (id)window->winId(); +#else + switch (obs_get_nix_platform()) { + case OBS_NIX_PLATFORM_X11_EGL: + gswindow.id = window->winId(); + gswindow.display = obs_get_nix_platform_display(); + break; +#ifdef ENABLE_WAYLAND + case OBS_NIX_PLATFORM_WAYLAND: { + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + gswindow.display = native->nativeResourceForWindow("surface", window); + success = gswindow.display != nullptr; + break; + } +#endif + default: + success = false; + break; + } +#endif + return success; +} + +OBSQTDisplay::OBSQTDisplay(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags) +{ + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_StaticContents); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + + auto windowVisible = [this](bool visible) { + if (!visible) { +#if !defined(_WIN32) && !defined(__APPLE__) + display = nullptr; +#endif + return; + } + + if (!display) { + CreateDisplay(); + } else { + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + } + }; + + auto screenChanged = [this](QScreen *) { + CreateDisplay(); + + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + }; + + connect(windowHandle(), &QWindow::visibleChanged, windowVisible); + connect(windowHandle(), &QWindow::screenChanged, screenChanged); + + windowHandle()->installEventFilter(new SurfaceEventFilter(this)); +} + +QColor OBSQTDisplay::GetDisplayBackgroundColor() const +{ + return rgba_to_color(backgroundColor); +} + +void OBSQTDisplay::SetDisplayBackgroundColor(const QColor &color) +{ + uint32_t newBackgroundColor = (uint32_t)color_to_int(color); + + if (newBackgroundColor != backgroundColor) { + backgroundColor = newBackgroundColor; + UpdateDisplayBackgroundColor(); + } +} + +void OBSQTDisplay::UpdateDisplayBackgroundColor() +{ + obs_display_set_background_color(display, backgroundColor); +} + +void OBSQTDisplay::CreateDisplay() +{ + if (display) + return; + + if (destroying) + return; + + if (!windowHandle()->isExposed()) + return; + + QSize size = GetPixelSize(this); + + gs_init_data info = {}; + info.cx = size.width(); + info.cy = size.height(); + info.format = GS_BGRA; + info.zsformat = GS_ZS_NONE; + + if (!QTToGSWindow(windowHandle(), info.window)) + return; + + display = obs_display_create(&info, backgroundColor); + + emit DisplayCreated(this); +} + +void OBSQTDisplay::paintEvent(QPaintEvent *event) +{ + CreateDisplay(); + + QWidget::paintEvent(event); +} + +void OBSQTDisplay::moveEvent(QMoveEvent *event) +{ + QWidget::moveEvent(event); + + OnMove(); +} + +bool OBSQTDisplay::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_DISPLAYCHANGE: + OnDisplayChange(); + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSQTDisplay::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + CreateDisplay(); + + if (isVisible() && display) { + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + } + + emit DisplayResized(); +} + +QPaintEngine *OBSQTDisplay::paintEngine() const +{ + return nullptr; +} + +void OBSQTDisplay::OnMove() +{ + if (display) + obs_display_update_color_space(display); +} + +void OBSQTDisplay::OnDisplayChange() +{ + if (display) + obs_display_update_color_space(display); +} diff --git a/UI/qt-display.hpp b/frontend/widgets/OBSQTDisplay.hpp similarity index 100% rename from UI/qt-display.hpp rename to frontend/widgets/OBSQTDisplay.hpp diff --git a/frontend/widgets/StatusBarWidget.cpp b/frontend/widgets/StatusBarWidget.cpp new file mode 100644 index 000000000..5bcae6f33 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.cpp @@ -0,0 +1,601 @@ +#include +#include +#include "obs-app.hpp" +#include "window-basic-main.hpp" +#include "moc_window-basic-status-bar.cpp" +#include "window-basic-main-outputs.hpp" +#include "qt-wrappers.hpp" +#include "platform.hpp" + +#include "ui_StatusBarWidget.h" + +static constexpr int bitrateUpdateSeconds = 2; +static constexpr int congestionUpdateSeconds = 4; +static constexpr float excellentThreshold = 0.0f; +static constexpr float goodThreshold = 0.3333f; +static constexpr float mediocreThreshold = 0.6667f; +static constexpr float badThreshold = 1.0f; + +StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) +{ + ui->setupUi(this); +} + +StatusBarWidget::~StatusBarWidget() {} + +OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) + : QStatusBar(parent), + excellentPixmap(QIcon(":/res/images/network-excellent.svg").pixmap(QSize(16, 16))), + goodPixmap(QIcon(":/res/images/network-good.svg").pixmap(QSize(16, 16))), + mediocrePixmap(QIcon(":/res/images/network-mediocre.svg").pixmap(QSize(16, 16))), + badPixmap(QIcon(":/res/images/network-bad.svg").pixmap(QSize(16, 16))), + recordingActivePixmap(QIcon(":/res/images/recording-active.svg").pixmap(QSize(16, 16))), + recordingPausePixmap(QIcon(":/res/images/recording-pause.svg").pixmap(QSize(16, 16))), + streamingActivePixmap(QIcon(":/res/images/streaming-active.svg").pixmap(QSize(16, 16))) +{ + congestionArray.reserve(congestionUpdateSeconds); + + statusWidget = new StatusBarWidget(this); + statusWidget->ui->delayInfo->setText(""); + statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->streamTime->setDisabled(true); + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); + statusWidget->ui->recordTime->setDisabled(true); + statusWidget->ui->delayFrame->hide(); + statusWidget->ui->issuesFrame->hide(); + statusWidget->ui->kbps->hide(); + + addPermanentWidget(statusWidget, 1); + setMinimumHeight(statusWidget->height()); + + UpdateIcons(); + connect(App(), &OBSApp::StyleChanged, this, &OBSBasicStatusBar::UpdateIcons); + + messageTimer = new QTimer(this); + messageTimer->setSingleShot(true); + connect(messageTimer, &QTimer::timeout, this, &OBSBasicStatusBar::clearMessage); + + clearMessage(); +} + +void OBSBasicStatusBar::Activate() +{ + if (!active) { + refreshTimer = new QTimer(this); + connect(refreshTimer, &QTimer::timeout, this, &OBSBasicStatusBar::UpdateStatusBar); + + int skipped = video_output_get_skipped_frames(obs_get_video()); + int total = video_output_get_total_frames(obs_get_video()); + + totalStreamSeconds = 0; + totalRecordSeconds = 0; + lastSkippedFrameCount = 0; + startSkippedFrameCount = skipped; + startTotalFrameCount = total; + + refreshTimer->start(1000); + active = true; + + if (streamOutput) { + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } + } + + if (streamOutput) { + statusWidget->ui->streamIcon->setPixmap(streamingActivePixmap); + statusWidget->ui->streamTime->setDisabled(false); + statusWidget->ui->issuesFrame->show(); + statusWidget->ui->kbps->show(); + firstCongestionUpdate = true; + } + + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); + statusWidget->ui->recordTime->setDisabled(false); + } +} + +void OBSBasicStatusBar::Deactivate() +{ + OBSBasic *main = qobject_cast(parent()); + if (!main) + return; + + if (!streamOutput) { + statusWidget->ui->streamTime->setText(QString("00:00:00")); + statusWidget->ui->streamTime->setDisabled(true); + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + statusWidget->ui->delayFrame->hide(); + statusWidget->ui->issuesFrame->hide(); + statusWidget->ui->kbps->hide(); + totalStreamSeconds = 0; + congestionArray.clear(); + disconnected = false; + firstCongestionUpdate = false; + } + + if (!recordOutput) { + statusWidget->ui->recordTime->setText(QString("00:00:00")); + statusWidget->ui->recordTime->setDisabled(true); + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); + totalRecordSeconds = 0; + } + + if (main->outputHandler && !main->outputHandler->Active()) { + delete refreshTimer; + + statusWidget->ui->delayInfo->setText(""); + statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); + statusWidget->ui->kbps->setText("0 kbps"); + + delaySecTotal = 0; + delaySecStarting = 0; + delaySecStopping = 0; + reconnectTimeout = 0; + active = false; + overloadedNotify = true; + + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } +} + +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)); + } + + if (!statusWidget->ui->delayFrame->isVisible()) + statusWidget->ui->delayFrame->show(); + + statusWidget->ui->delayInfo->setText(msg); + } +} + +void OBSBasicStatusBar::UpdateBandwidth() +{ + if (!streamOutput) + return; + + if (++seconds < bitrateUpdateSeconds) + return; + + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + uint64_t bytesSent = obs_output_get_total_bytes(output); + uint64_t bytesSentTime = os_gettime_ns(); + + if (bytesSent < lastBytesSent) + bytesSent = 0; + if (bytesSent == 0) + lastBytesSent = 0; + + uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; + + double timePassed = double(bytesSentTime - lastBytesSentTime) / 1000000000.0; + + double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0; + + QString text; + text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps"); + + statusWidget->ui->kbps->setText(text); + statusWidget->ui->kbps->setMinimumWidth(statusWidget->ui->kbps->width()); + + if (!statusWidget->ui->kbps->isVisible()) + statusWidget->ui->kbps->show(); + + lastBytesSent = bytesSent; + lastBytesSentTime = bytesSentTime; + seconds = 0; +} + +void OBSBasicStatusBar::UpdateCPUUsage() +{ + OBSBasic *main = qobject_cast(parent()); + if (!main) + return; + + QString text; + text += QString("CPU: ") + QString::number(main->GetCPUUsage(), 'f', 1) + QString("%"); + + statusWidget->ui->cpuUsage->setText(text); + statusWidget->ui->cpuUsage->setMinimumWidth(statusWidget->ui->cpuUsage->width()); + + UpdateCurrentFPS(); +} + +void OBSBasicStatusBar::UpdateCurrentFPS() +{ + struct obs_video_info ovi; + obs_get_video_info(&ovi); + float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den; + + QString text = QString::asprintf("%.2f / %.2f FPS", obs_get_active_fps(), targetFPS); + + statusWidget->ui->fpsCurrent->setText(text); + statusWidget->ui->fpsCurrent->setMinimumWidth(statusWidget->ui->fpsCurrent->width()); +} + +void OBSBasicStatusBar::UpdateStreamTime() +{ + totalStreamSeconds++; + + int seconds = totalStreamSeconds % 60; + int totalMinutes = totalStreamSeconds / 60; + int minutes = totalMinutes % 60; + int hours = totalMinutes / 60; + + QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + statusWidget->ui->streamTime->setText(text); + if (streamOutput && !statusWidget->ui->streamTime->isEnabled()) + statusWidget->ui->streamTime->setDisabled(false); + + if (reconnectTimeout > 0) { + QString msg = QTStr("Basic.StatusBar.Reconnecting") + .arg(QString::number(retries), QString::number(reconnectTimeout)); + showMessage(msg); + disconnected = true; + statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); + congestionArray.clear(); + reconnectTimeout--; + + } else if (retries > 0) { + 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(); + } +} + +extern volatile bool recording_paused; + +void OBSBasicStatusBar::UpdateRecordTime() +{ + bool paused = os_atomic_load_bool(&recording_paused); + + if (!paused) { + totalRecordSeconds++; + + if (recordOutput && !statusWidget->ui->recordTime->isEnabled()) + statusWidget->ui->recordTime->setDisabled(false); + } else { + statusWidget->ui->recordIcon->setPixmap(streamPauseIconToggle ? recordingPauseInactivePixmap + : recordingPausePixmap); + + streamPauseIconToggle = !streamPauseIconToggle; + } + + UpdateRecordTimeLabel(); +} + +void OBSBasicStatusBar::UpdateRecordTimeLabel() +{ + int seconds = totalRecordSeconds % 60; + int totalMinutes = totalRecordSeconds / 60; + int minutes = totalMinutes % 60; + int hours = totalMinutes / 60; + + QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + if (os_atomic_load_bool(&recording_paused)) { + text += QStringLiteral(" (PAUSED)"); + } + + statusWidget->ui->recordTime->setText(text); +} + +void OBSBasicStatusBar::UpdateDroppedFrames() +{ + if (!streamOutput) + return; + + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + int totalDropped = obs_output_get_frames_dropped(output); + int totalFrames = obs_output_get_total_frames(output); + double percent = (double)totalDropped / (double)totalFrames * 100.0; + + if (!totalFrames) + return; + + QString text = QTStr("DroppedFrames"); + text = text.arg(QString::number(totalDropped), QString::number(percent, 'f', 1)); + statusWidget->ui->droppedFrames->setText(text); + + if (!statusWidget->ui->issuesFrame->isVisible()) + statusWidget->ui->issuesFrame->show(); + + /* ----------------------------------- * + * calculate congestion color */ + + float congestion = obs_output_get_congestion(output); + float avgCongestion = (congestion + lastCongestion) * 0.5f; + if (avgCongestion < congestion) + avgCongestion = congestion; + if (avgCongestion > 1.0f) + avgCongestion = 1.0f; + + lastCongestion = congestion; + + if (disconnected) + return; + + bool update = firstCongestionUpdate; + float congestionOverTime = avgCongestion; + + if (congestionArray.size() >= congestionUpdateSeconds) { + congestionOverTime = accumulate(congestionArray.begin(), congestionArray.end(), 0.0f) / + (float)congestionArray.size(); + congestionArray.clear(); + update = true; + } else { + congestionArray.emplace_back(avgCongestion); + } + + if (update) { + if (congestionOverTime <= excellentThreshold + EPSILON) + statusWidget->ui->statusIcon->setPixmap(excellentPixmap); + else if (congestionOverTime <= goodThreshold) + statusWidget->ui->statusIcon->setPixmap(goodPixmap); + else if (congestionOverTime <= mediocreThreshold) + statusWidget->ui->statusIcon->setPixmap(mediocrePixmap); + else if (congestionOverTime <= badThreshold) + statusWidget->ui->statusIcon->setPixmap(badPixmap); + + firstCongestionUpdate = false; + } +} + +void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params) +{ + OBSBasicStatusBar *statusBar = reinterpret_cast(data); + + int seconds = (int)calldata_int(params, "timeout_sec"); + QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds)); +} + +void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *) +{ + OBSBasicStatusBar *statusBar = reinterpret_cast(data); + + QMetaObject::invokeMethod(statusBar, "ReconnectSuccess"); +} + +void OBSBasicStatusBar::Reconnect(int seconds) +{ + OBSBasic *main = qobject_cast(parent()); + + if (!retries) + main->SysTrayNotify(QTStr("Basic.SystemTray.Message.Reconnecting"), QSystemTrayIcon::Warning); + + reconnectTimeout = seconds; + + if (streamOutput) { + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + delaySecTotal = obs_output_get_active_delay(output); + UpdateDelayMsg(); + + retries++; + } +} + +void OBSBasicStatusBar::ReconnectClear() +{ + retries = 0; + reconnectTimeout = 0; + seconds = -1; + lastBytesSent = 0; + lastBytesSentTime = os_gettime_ns(); + delaySecTotal = 0; + UpdateDelayMsg(); +} + +void OBSBasicStatusBar::ReconnectSuccess() +{ + OBSBasic *main = qobject_cast(parent()); + + QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful"); + showMessage(msg, 4000); + main->SysTrayNotify(msg, QSystemTrayIcon::Information); + ReconnectClear(); + + if (streamOutput) { + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + delaySecTotal = obs_output_get_active_delay(output); + UpdateDelayMsg(); + disconnected = false; + firstCongestionUpdate = true; + } +} + +void OBSBasicStatusBar::UpdateStatusBar() +{ + OBSBasic *main = qobject_cast(parent()); + + UpdateBandwidth(); + + if (streamOutput) + UpdateStreamTime(); + + if (recordOutput) + UpdateRecordTime(); + + UpdateDroppedFrames(); + + int skipped = video_output_get_skipped_frames(obs_get_video()); + int total = video_output_get_total_frames(obs_get_video()); + + skipped -= startSkippedFrameCount; + total -= startTotalFrameCount; + + int diff = skipped - lastSkippedFrameCount; + double percentage = double(skipped) / double(total) * 100.0; + + if (diff > 10 && percentage >= 0.1f) { + showMessage(QTStr("HighResourceUsage"), 4000); + if (!main->isVisible() && overloadedNotify) { + main->SysTrayNotify(QTStr("HighResourceUsage"), QSystemTrayIcon::Warning); + overloadedNotify = false; + } + } + + lastSkippedFrameCount = skipped; +} + +void OBSBasicStatusBar::StreamDelayStarting(int sec) +{ + OBSBasic *main = qobject_cast(parent()); + if (!main || !main->outputHandler) + return; + + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + streamOutput = OBSGetWeakRef(output); + + delaySecTotal = delaySecStarting = sec; + UpdateDelayMsg(); + Activate(); +} + +void OBSBasicStatusBar::StreamDelayStopping(int sec) +{ + delaySecTotal = delaySecStopping = sec; + UpdateDelayMsg(); +} + +void OBSBasicStatusBar::StreamStarted(obs_output_t *output) +{ + streamOutput = OBSGetWeakRef(output); + + streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect", OBSOutputReconnect, this); + streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect_success", OBSOutputReconnectSuccess, + this); + + retries = 0; + lastBytesSent = 0; + lastBytesSentTime = os_gettime_ns(); + Activate(); +} + +void OBSBasicStatusBar::StreamStopped() +{ + if (streamOutput) { + streamSigs.clear(); + + ReconnectClear(); + streamOutput = nullptr; + clearMessage(); + Deactivate(); + } +} + +void OBSBasicStatusBar::RecordingStarted(obs_output_t *output) +{ + recordOutput = OBSGetWeakRef(output); + Activate(); +} + +void OBSBasicStatusBar::RecordingStopped() +{ + recordOutput = nullptr; + Deactivate(); +} + +void OBSBasicStatusBar::RecordingPaused() +{ + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap); + streamPauseIconToggle = true; + } + + UpdateRecordTimeLabel(); +} + +void OBSBasicStatusBar::RecordingUnpaused() +{ + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); + } + + UpdateRecordTimeLabel(); +} + +static QPixmap GetPixmap(const QString &filename) +{ + QString path = obs_frontend_is_theme_dark() ? "theme:Dark/" : ":/res/images/"; + return QIcon(path + filename).pixmap(QSize(16, 16)); +} + +void OBSBasicStatusBar::UpdateIcons() +{ + disconnectedPixmap = GetPixmap("network-disconnected.svg"); + inactivePixmap = GetPixmap("network-inactive.svg"); + + streamingInactivePixmap = GetPixmap("streaming-inactive.svg"); + + recordingInactivePixmap = GetPixmap("recording-inactive.svg"); + recordingPauseInactivePixmap = GetPixmap("recording-pause-inactive.svg"); + + bool streaming = obs_frontend_streaming_active(); + + if (!streaming) { + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } else { + if (disconnected) + statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); + } + + bool recording = obs_frontend_recording_active(); + + if (!recording) + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); +} + +void OBSBasicStatusBar::showMessage(const QString &message, int timeout) +{ + messageTimer->stop(); + + statusWidget->ui->message->setText(message); + + if (timeout) + messageTimer->start(timeout); +} + +void OBSBasicStatusBar::clearMessage() +{ + statusWidget->ui->message->setText(""); +} diff --git a/frontend/widgets/StatusBarWidget.hpp b/frontend/widgets/StatusBarWidget.hpp new file mode 100644 index 000000000..38be9e520 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Ui_StatusBarWidget; + +class StatusBarWidget : public QWidget { + Q_OBJECT + + friend class OBSBasicStatusBar; + +private: + std::unique_ptr ui; + +public: + StatusBarWidget(QWidget *parent = nullptr); + ~StatusBarWidget(); +}; + +class OBSBasicStatusBar : public QStatusBar { + Q_OBJECT + +private: + StatusBarWidget *statusWidget = nullptr; + + OBSWeakOutputAutoRelease streamOutput; + std::vector streamSigs; + OBSWeakOutputAutoRelease recordOutput; + bool active = false; + bool overloadedNotify = true; + bool streamPauseIconToggle = false; + bool disconnected = false; + bool firstCongestionUpdate = false; + + std::vector congestionArray; + + int retries = 0; + int totalStreamSeconds = 0; + int totalRecordSeconds = 0; + + int reconnectTimeout = 0; + + int delaySecTotal = 0; + int delaySecStarting = 0; + int delaySecStopping = 0; + + int startSkippedFrameCount = 0; + int startTotalFrameCount = 0; + int lastSkippedFrameCount = 0; + + int seconds = 0; + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + + QPixmap excellentPixmap; + QPixmap goodPixmap; + QPixmap mediocrePixmap; + QPixmap badPixmap; + QPixmap disconnectedPixmap; + QPixmap inactivePixmap; + + QPixmap recordingActivePixmap; + QPixmap recordingPausePixmap; + QPixmap recordingPauseInactivePixmap; + QPixmap recordingInactivePixmap; + QPixmap streamingActivePixmap; + QPixmap streamingInactivePixmap; + + float lastCongestion = 0.0f; + + QPointer refreshTimer; + QPointer messageTimer; + + obs_output_t *GetOutput(); + + void Activate(); + void Deactivate(); + + void UpdateDelayMsg(); + void UpdateBandwidth(); + void UpdateStreamTime(); + void UpdateRecordTime(); + void UpdateRecordTimeLabel(); + void UpdateDroppedFrames(); + + static void OBSOutputReconnect(void *data, calldata_t *params); + static void OBSOutputReconnectSuccess(void *data, calldata_t *params); + +public slots: + void UpdateCPUUsage(); + + void clearMessage(); + void showMessage(const QString &message, int timeout = 0); + +private slots: + void Reconnect(int seconds); + void ReconnectSuccess(); + void UpdateStatusBar(); + void UpdateCurrentFPS(); + void UpdateIcons(); + +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); + void RecordingStopped(); + void RecordingPaused(); + void RecordingUnpaused(); + + void ReconnectClear(); +}; diff --git a/frontend/widgets/VolControl.cpp b/frontend/widgets/VolControl.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolControl.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolControl.hpp b/frontend/widgets/VolControl.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolControl.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/VolumeAccessibleInterface.cpp b/frontend/widgets/VolumeAccessibleInterface.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolumeAccessibleInterface.hpp b/frontend/widgets/VolumeAccessibleInterface.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/VolumeMeter.cpp b/frontend/widgets/VolumeMeter.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolumeMeter.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolumeMeter.hpp b/frontend/widgets/VolumeMeter.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolumeMeter.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; From 654ddcd40984dbca386e02fc28056f430fc18ae3 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 17:43:35 +0100 Subject: [PATCH 23/37] frontend: Split Qt UI Widget implementations into single files per class --- frontend/components/VolumeSlider.cpp | 1431 +-- frontend/components/VolumeSlider.hpp | 320 +- frontend/utility/AdvancedOutput.cpp | 1637 +-- frontend/utility/AdvancedOutput.hpp | 2498 +--- frontend/utility/BasicOutputHandler.cpp | 2011 +-- frontend/utility/BasicOutputHandler.hpp | 51 +- frontend/utility/QuickTransition.cpp | 1662 +-- frontend/utility/QuickTransition.hpp | 1338 +- frontend/utility/SceneRenameDelegate.cpp | 10156 +-------------- frontend/utility/SceneRenameDelegate.hpp | 1351 -- frontend/utility/ScreenshotObj.cpp | 43 +- frontend/utility/SimpleOutput.cpp | 1651 +-- frontend/utility/SimpleOutput.hpp | 2486 +--- .../StartMultiTrackVideoStreamingGuard.hpp | 2525 +--- frontend/utility/SurfaceEventFilter.hpp | 213 +- frontend/utility/VolumeMeterTimer.cpp | 1485 +-- frontend/utility/VolumeMeterTimer.hpp | 324 +- frontend/widgets/ColorSelect.cpp | 10180 +--------------- frontend/widgets/ColorSelect.hpp | 1353 +- frontend/widgets/OBSBasic.cpp | 8337 +------------ frontend/widgets/OBSBasic.hpp | 2234 ++-- frontend/widgets/OBSBasicStatusBar.cpp | 21 +- frontend/widgets/OBSBasicStatusBar.hpp | 24 +- frontend/widgets/OBSBasic_Browser.cpp | 25 +- frontend/widgets/OBSBasic_Clipboard.cpp | 9964 +-------------- frontend/widgets/OBSBasic_ContextToolbar.cpp | 9936 +-------------- frontend/widgets/OBSBasic_Docks.cpp | 9856 +-------------- frontend/widgets/OBSBasic_Dropfiles.cpp | 33 +- frontend/widgets/OBSBasic_Hotkeys.cpp | 9897 +-------------- frontend/widgets/OBSBasic_Icons.cpp | 21 +- frontend/widgets/OBSBasic_MainControls.cpp | 9589 +-------------- frontend/widgets/OBSBasic_OutputHandler.cpp | 10070 +-------------- frontend/widgets/OBSBasic_Preview.cpp | 9556 +-------------- frontend/widgets/OBSBasic_Profiles.cpp | 26 +- frontend/widgets/OBSBasic_Projectors.cpp | 9933 +-------------- frontend/widgets/OBSBasic_Recording.cpp | 9809 +-------------- frontend/widgets/OBSBasic_ReplayBuffer.cpp | 9999 +-------------- frontend/widgets/OBSBasic_SceneItems.cpp | 8818 +------------ frontend/widgets/OBSBasic_Scenes.cpp | 9248 +------------- frontend/widgets/OBSBasic_Screenshots.cpp | 299 +- frontend/widgets/OBSBasic_Service.cpp | 10083 +-------------- frontend/widgets/OBSBasic_StatusBar.cpp | 10179 +-------------- frontend/widgets/OBSBasic_Streaming.cpp | 9775 +-------------- frontend/widgets/OBSBasic_SysTray.cpp | 10060 +-------------- frontend/widgets/OBSBasic_Updater.cpp | 9997 +-------------- frontend/widgets/OBSBasic_VirtualCam.cpp | 10037 +-------------- frontend/widgets/OBSBasic_VolControl.cpp | 9890 +-------------- frontend/widgets/OBSBasic_YouTube.cpp | 9961 +-------------- frontend/widgets/OBSQTDisplay.cpp | 61 +- frontend/widgets/OBSQTDisplay.hpp | 3 +- frontend/widgets/StatusBarWidget.cpp | 595 +- frontend/widgets/StatusBarWidget.hpp | 104 +- frontend/widgets/VolControl.cpp | 1123 +- frontend/widgets/VolControl.hpp | 285 +- .../widgets/VolumeAccessibleInterface.cpp | 1450 +-- .../widgets/VolumeAccessibleInterface.hpp | 315 - frontend/widgets/VolumeMeter.cpp | 567 +- frontend/widgets/VolumeMeter.hpp | 129 +- 58 files changed, 1723 insertions(+), 253301 deletions(-) diff --git a/frontend/components/VolumeSlider.cpp b/frontend/components/VolumeSlider.cpp index cd00c14d7..4ef9b5d22 100644 --- a/frontend/components/VolumeSlider.cpp +++ b/frontend/components/VolumeSlider.cpp @@ -1,1377 +1,8 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolumeSlider.hpp" -#include -#include -#include -#include -#include -#include #include -using namespace std; - -#define FADER_PRECISION 4096.0 - -// Size of the audio indicator in pixels -#define INDICATOR_THICKNESS 3 - -// Padding on top and bottom of vertical meters -#define METER_PADDING 1 - -std::weak_ptr VolumeMeter::updateTimer; - -static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) -{ - if (muted) - return Qt::Checked; - else if (unassigned) - return Qt::PartiallyChecked; - else - return Qt::Unchecked; -} - -static inline bool IsSourceUnassigned(obs_source_t *source) -{ - uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); - obs_monitoring_type mt = obs_source_get_monitoring_type(source); - - return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; -} - -static void ShowUnassignedWarning(const char *name) -{ - auto msgBox = [=]() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); - msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); -} - -void VolControl::OBSVolumeChanged(void *data, float db) -{ - Q_UNUSED(db); - VolControl *volControl = static_cast(data); - - QMetaObject::invokeMethod(volControl, "VolumeChanged"); -} - -void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - VolControl *volControl = static_cast(data); - - volControl->volMeter->setLevels(magnitude, peak, inputPeak); -} - -void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) -{ - VolControl *volControl = static_cast(data); - bool muted = calldata_bool(calldata, "muted"); - - QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); -} - -void VolControl::VolumeChanged() -{ - slider->blockSignals(true); - slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); - slider->blockSignals(false); - - updateText(); -} - -void VolControl::VolumeMuted(bool muted) -{ - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) -{ - - VolControl *volControl = static_cast(data); - QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); -} - -void VolControl::MixersOrMonitoringChanged() -{ - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::SetMuted(bool) -{ - bool checked = mute->checkState() == Qt::Checked; - bool prev = obs_source_muted(source); - obs_source_set_muted(source, checked); - bool unassigned = IsSourceUnassigned(source); - - if (!checked && unassigned) { - mute->setCheckState(Qt::PartiallyChecked); - /* Show notice about the source no being assigned to any tracks */ - bool has_shown_warning = - config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); - if (!has_shown_warning) - ShowUnassignedWarning(obs_source_get_name(source)); - } - - auto undo_redo = [](const std::string &uuid, bool val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_muted(source, val); - }; - - QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); - - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); -} - -void VolControl::SliderChanged(int vol) -{ - float prev = obs_source_get_volume(source); - - obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); - updateText(); - - auto undo_redo = [](const std::string &uuid, float val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_volume(source, val); - }; - - float val = obs_source_get_volume(source); - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), - std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); -} - -void VolControl::updateText() -{ - QString text; - float db = obs_fader_get_db(obs_fader); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - volLabel->setText(text); - - bool muted = obs_source_muted(source); - const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; - - QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName); - - slider->setAccessibleName(accText); -} - -void VolControl::EmitConfigClicked() -{ - emit ConfigClicked(); -} - -void VolControl::SetMeterDecayRate(qreal q) -{ - volMeter->setPeakDecayRate(q); -} - -void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - volMeter->setPeakMeterType(peakMeterType); -} - -VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) - : source(std::move(source_)), - levelTotal(0.0f), - levelCount(0.0f), - obs_fader(obs_fader_create(OBS_FADER_LOG)), - obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), - vertical(vertical), - contextMenu(nullptr) -{ - nameLabel = new OBSSourceLabel(source); - volLabel = new QLabel(); - mute = new MuteCheckBox(); - - volLabel->setObjectName("volLabel"); - volLabel->setAlignment(Qt::AlignCenter); - -#ifdef __APPLE__ - mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - QString sourceName = obs_source_get_name(source); - setObjectName(sourceName); - - if (showConfig) { - config = new QPushButton(this); - config->setProperty("class", "icon-dots-vert"); - config->setAutoDefault(false); - - config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); - - connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - } - - QVBoxLayout *mainLayout = new QVBoxLayout; - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - if (vertical) { - QHBoxLayout *nameLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QHBoxLayout *volLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QHBoxLayout *meterLayout = new QHBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new VolumeSlider(obs_fader, Qt::Vertical); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - nameLayout->setAlignment(Qt::AlignCenter); - meterLayout->setAlignment(Qt::AlignCenter); - controlLayout->setAlignment(Qt::AlignCenter); - volLayout->setAlignment(Qt::AlignCenter); - - meterFrame->setObjectName("volMeterFrame"); - - nameLayout->setContentsMargins(0, 0, 0, 0); - nameLayout->setSpacing(0); - nameLayout->addWidget(nameLabel); - - controlLayout->setContentsMargins(0, 0, 0, 0); - controlLayout->setSpacing(0); - - // Add Headphone (audio monitoring) widget here - controlLayout->addWidget(mute); - - if (showConfig) { - controlLayout->addWidget(config); - } - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - meterLayout->addWidget(slider); - meterLayout->addWidget(volMeter); - - meterFrame->setLayout(meterLayout); - - volLayout->setContentsMargins(0, 0, 0, 0); - volLayout->setSpacing(0); - volLayout->addWidget(volLabel); - volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); - - mainLayout->addItem(nameLayout); - mainLayout->addItem(volLayout); - mainLayout->addWidget(meterFrame); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - - // Default size can cause clipping of long names in vertical layout. - QFont font = nameLabel->font(); - QFontInfo info(font); - nameLabel->setFont(font); - - setMaximumWidth(110); - } else { - QHBoxLayout *textLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QVBoxLayout *meterLayout = new QVBoxLayout; - QVBoxLayout *buttonLayout = new QVBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - slider = new VolumeSlider(obs_fader, Qt::Horizontal); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - meterFrame->setObjectName("volMeterFrame"); - meterFrame->setLayout(meterLayout); - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - - meterLayout->addWidget(volMeter); - meterLayout->addWidget(slider); - - buttonLayout->setContentsMargins(0, 0, 0, 0); - buttonLayout->setSpacing(0); - - if (showConfig) { - buttonLayout->addWidget(config); - } - buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); - buttonLayout->addWidget(mute); - - controlLayout->addItem(buttonLayout); - controlLayout->addWidget(meterFrame); - - mainLayout->addItem(textLayout); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - } - - setLayout(mainLayout); - - nameLabel->setText(sourceName); - - slider->setMinimum(0); - slider->setMaximum(int(FADER_PRECISION)); - - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - mute->setCheckState(GetCheckState(muted, unassigned)); - volMeter->muted = muted || unassigned; - mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); - obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, - this); - - QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); - QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); - - obs_fader_attach_source(obs_fader, source); - obs_volmeter_attach_source(obs_volmeter, source); - - /* Call volume changed once to init the slider position and label */ - VolumeChanged(); -} - -void VolControl::EnableSlider(bool enable) -{ - slider->setEnabled(enable); -} - -VolControl::~VolControl() -{ - obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.clear(); - - if (contextMenu) - contextMenu->close(); -} - -static inline QColor color_from_int(long long val) -{ - QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); - color.setAlpha(255); - - return color; -} - -QColor VolumeMeter::getBackgroundNominalColor() const -{ - return p_backgroundNominalColor; -} - -QColor VolumeMeter::getBackgroundNominalColorDisabled() const -{ - return backgroundNominalColorDisabled; -} - -void VolumeMeter::setBackgroundNominalColor(QColor c) -{ - p_backgroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); - } else { - backgroundNominalColor = p_backgroundNominalColor; - } -} - -void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) -{ - backgroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundWarningColor() const -{ - return p_backgroundWarningColor; -} - -QColor VolumeMeter::getBackgroundWarningColorDisabled() const -{ - return backgroundWarningColorDisabled; -} - -void VolumeMeter::setBackgroundWarningColor(QColor c) -{ - p_backgroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); - } else { - backgroundWarningColor = p_backgroundWarningColor; - } -} - -void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) -{ - backgroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundErrorColor() const -{ - return p_backgroundErrorColor; -} - -QColor VolumeMeter::getBackgroundErrorColorDisabled() const -{ - return backgroundErrorColorDisabled; -} - -void VolumeMeter::setBackgroundErrorColor(QColor c) -{ - p_backgroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); - } else { - backgroundErrorColor = p_backgroundErrorColor; - } -} - -void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) -{ - backgroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundNominalColor() const -{ - return p_foregroundNominalColor; -} - -QColor VolumeMeter::getForegroundNominalColorDisabled() const -{ - return foregroundNominalColorDisabled; -} - -void VolumeMeter::setForegroundNominalColor(QColor c) -{ - p_foregroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); - } else { - foregroundNominalColor = p_foregroundNominalColor; - } -} - -void VolumeMeter::setForegroundNominalColorDisabled(QColor c) -{ - foregroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundWarningColor() const -{ - return p_foregroundWarningColor; -} - -QColor VolumeMeter::getForegroundWarningColorDisabled() const -{ - return foregroundWarningColorDisabled; -} - -void VolumeMeter::setForegroundWarningColor(QColor c) -{ - p_foregroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); - } else { - foregroundWarningColor = p_foregroundWarningColor; - } -} - -void VolumeMeter::setForegroundWarningColorDisabled(QColor c) -{ - foregroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundErrorColor() const -{ - return p_foregroundErrorColor; -} - -QColor VolumeMeter::getForegroundErrorColorDisabled() const -{ - return foregroundErrorColorDisabled; -} - -void VolumeMeter::setForegroundErrorColor(QColor c) -{ - p_foregroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); - } else { - foregroundErrorColor = p_foregroundErrorColor; - } -} - -void VolumeMeter::setForegroundErrorColorDisabled(QColor c) -{ - foregroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getClipColor() const -{ - return clipColor; -} - -void VolumeMeter::setClipColor(QColor c) -{ - clipColor = std::move(c); -} - -QColor VolumeMeter::getMagnitudeColor() const -{ - return magnitudeColor; -} - -void VolumeMeter::setMagnitudeColor(QColor c) -{ - magnitudeColor = std::move(c); -} - -QColor VolumeMeter::getMajorTickColor() const -{ - return majorTickColor; -} - -void VolumeMeter::setMajorTickColor(QColor c) -{ - majorTickColor = std::move(c); -} - -QColor VolumeMeter::getMinorTickColor() const -{ - return minorTickColor; -} - -void VolumeMeter::setMinorTickColor(QColor c) -{ - minorTickColor = std::move(c); -} - -int VolumeMeter::getMeterThickness() const -{ - return meterThickness; -} - -void VolumeMeter::setMeterThickness(int v) -{ - meterThickness = v; - recalculateLayout = true; -} - -qreal VolumeMeter::getMeterFontScaling() const -{ - return meterFontScaling; -} - -void VolumeMeter::setMeterFontScaling(qreal v) -{ - meterFontScaling = v; - recalculateLayout = true; -} - -void VolControl::refreshColors() -{ - volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); - volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); - volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); - volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); - volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); - volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); -} - -qreal VolumeMeter::getMinimumLevel() const -{ - return minimumLevel; -} - -void VolumeMeter::setMinimumLevel(qreal v) -{ - minimumLevel = v; -} - -qreal VolumeMeter::getWarningLevel() const -{ - return warningLevel; -} - -void VolumeMeter::setWarningLevel(qreal v) -{ - warningLevel = v; -} - -qreal VolumeMeter::getErrorLevel() const -{ - return errorLevel; -} - -void VolumeMeter::setErrorLevel(qreal v) -{ - errorLevel = v; -} - -qreal VolumeMeter::getClipLevel() const -{ - return clipLevel; -} - -void VolumeMeter::setClipLevel(qreal v) -{ - clipLevel = v; -} - -qreal VolumeMeter::getMinimumInputLevel() const -{ - return minimumInputLevel; -} - -void VolumeMeter::setMinimumInputLevel(qreal v) -{ - minimumInputLevel = v; -} - -qreal VolumeMeter::getPeakDecayRate() const -{ - return peakDecayRate; -} - -void VolumeMeter::setPeakDecayRate(qreal v) -{ - peakDecayRate = v; -} - -qreal VolumeMeter::getMagnitudeIntegrationTime() const -{ - return magnitudeIntegrationTime; -} - -void VolumeMeter::setMagnitudeIntegrationTime(qreal v) -{ - magnitudeIntegrationTime = v; -} - -qreal VolumeMeter::getPeakHoldDuration() const -{ - return peakHoldDuration; -} - -void VolumeMeter::setPeakHoldDuration(qreal v) -{ - peakHoldDuration = v; -} - -qreal VolumeMeter::getInputPeakHoldDuration() const -{ - return inputPeakHoldDuration; -} - -void VolumeMeter::setInputPeakHoldDuration(qreal v) -{ - inputPeakHoldDuration = v; -} - -void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); - switch (peakMeterType) { - case TRUE_PEAK_METER: - // For true-peak meters EBU has defined the Permitted Maximum, - // taking into account the accuracy of the meter and further - // processing required by lossy audio compression. - // - // The alignment level was not specified, but I've adjusted - // it compared to a sample-peak meter. Incidentally Youtube - // uses this new Alignment Level as the maximum integrated - // loudness of a video. - // - // * Permitted Maximum Level (PML) = -2.0 dBTP - // * Alignment Level (AL) = -13 dBTP - setErrorLevel(-2.0); - setWarningLevel(-13.0); - break; - - case SAMPLE_PEAK_METER: - default: - // For a sample Peak Meter EBU has the following level - // definitions, taking into account inaccuracies of this meter: - // - // * Permitted Maximum Level (PML) = -9.0 dBFS - // * Alignment Level (AL) = -20.0 dBFS - setErrorLevel(-9.0); - setWarningLevel(-20.0); - break; - } -} - -void VolumeMeter::mousePressEvent(QMouseEvent *event) -{ - setFocus(Qt::MouseFocusReason); - event->accept(); -} - -void VolumeMeter::wheelEvent(QWheelEvent *event) -{ - QApplication::sendEvent(focusProxy(), event); -} - -VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) - : QWidget(parent), - obs_volmeter(obs_volmeter), - vertical(vertical) -{ - setAttribute(Qt::WA_OpaquePaintEvent, true); - - // Default meter settings, they only show if - // there is no stylesheet, do not remove. - backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green - backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow - backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red - foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green - foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow - foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red - - backgroundNominalColorDisabled.setRgb(90, 90, 90); - backgroundWarningColorDisabled.setRgb(117, 117, 117); - backgroundErrorColorDisabled.setRgb(65, 65, 65); - foregroundNominalColorDisabled.setRgb(163, 163, 163); - foregroundWarningColorDisabled.setRgb(217, 217, 217); - foregroundErrorColorDisabled.setRgb(113, 113, 113); - - clipColor.setRgb(0xff, 0xff, 0xff); // Bright white - magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black - majorTickColor.setRgb(0x00, 0x00, 0x00); // Black - minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray - minimumLevel = -60.0; // -60 dB - warningLevel = -20.0; // -20 dB - errorLevel = -9.0; // -9 dB - clipLevel = -0.5; // -0.5 dB - minimumInputLevel = -50.0; // -50 dB - peakDecayRate = 11.76; // 20 dB / 1.7 sec - magnitudeIntegrationTime = 0.3; // 99% in 300 ms - peakHoldDuration = 20.0; // 20 seconds - inputPeakHoldDuration = 1.0; // 1 second - meterThickness = 3; // Bar thickness in pixels - meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size - channels = (int)audio_output_get_channels(obs_get_audio()); - - doLayout(); - updateTimerRef = updateTimer.lock(); - if (!updateTimerRef) { - updateTimerRef = std::make_shared(); - updateTimerRef->setTimerType(Qt::PreciseTimer); - updateTimerRef->start(16); - updateTimer = updateTimerRef; - } - - updateTimerRef->AddVolControl(this); -} - -VolumeMeter::~VolumeMeter() -{ - updateTimerRef->RemoveVolControl(this); -} - -void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - uint64_t ts = os_gettime_ns(); - QMutexLocker locker(&dataMutex); - - currentLastUpdateTime = ts; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = magnitude[channelNr]; - currentPeak[channelNr] = peak[channelNr]; - currentInputPeak[channelNr] = inputPeak[channelNr]; - } - - // In case there are more updates then redraws we must make sure - // that the ballistics of peak and hold are recalculated. - locker.unlock(); - calculateBallistics(ts); -} - -inline void VolumeMeter::resetLevels() -{ - currentLastUpdateTime = 0; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = -M_INFINITE; - currentPeak[channelNr] = -M_INFINITE; - currentInputPeak[channelNr] = -M_INFINITE; - - displayMagnitude[channelNr] = -M_INFINITE; - displayPeak[channelNr] = -M_INFINITE; - displayPeakHold[channelNr] = -M_INFINITE; - displayPeakHoldLastUpdateTime[channelNr] = 0; - displayInputPeakHold[channelNr] = -M_INFINITE; - displayInputPeakHoldLastUpdateTime[channelNr] = 0; - } -} - -bool VolumeMeter::needLayoutChange() -{ - int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); - - if (!currentNrAudioChannels) { - struct obs_audio_info oai; - obs_get_audio_info(&oai); - currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; - } - - if (displayNrAudioChannels != currentNrAudioChannels) { - displayNrAudioChannels = currentNrAudioChannels; - recalculateLayout = true; - } - - return recalculateLayout; -} - -// When this is called from the constructor, obs_volmeter_get_nr_channels has not -// yet been called and Q_PROPERTY settings have not yet been read from the -// stylesheet. -inline void VolumeMeter::doLayout() -{ - QMutexLocker locker(&dataMutex); - - if (displayNrAudioChannels) { - int meterSize = std::floor(22 / displayNrAudioChannels); - setMeterThickness(std::clamp(meterSize, 3, 7)); - } - recalculateLayout = false; - - tickFont = font(); - QFontInfo info(tickFont); - tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); - QFontMetrics metrics(tickFont); - if (vertical) { - // Each meter channel is meterThickness pixels wide, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, space to hold our longest label in this font, - // and a few pixels before the fader. - QRect scaleBounds = metrics.boundingRect("-88"); - setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); - } else { - // Each meter channel is meterThickness pixels high, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, and space high enough to hold our label in - // this font, presuming that digits don't have descenders. - setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); - } - - resetLevels(); -} - -inline bool VolumeMeter::detectIdle(uint64_t ts) -{ - double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; - if (timeSinceLastUpdate > 0.5) { - resetLevels(); - return true; - } else { - return false; - } -} - -inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) -{ - if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { - // Attack of peak is immediate. - displayPeak[channelNr] = currentPeak[channelNr]; - } else { - // Decay of peak is 40 dB / 1.7 seconds for Fast Profile - // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) - // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) - float decay = float(peakDecayRate * timeSinceLastRedraw); - displayPeak[channelNr] = - std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); - } - - if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak - // after 20 seconds. - qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > peakHoldDuration) { - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || - !isfinite(displayInputPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak after 1 second. - qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > inputPeakHoldDuration) { - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (!isfinite(displayMagnitude[channelNr])) { - // The statements in the else-leg do not work with - // NaN and infinite displayMagnitude. - displayMagnitude[channelNr] = currentMagnitude[channelNr]; - } else { - // A VU meter will integrate to the new value to 99% in 300 ms. - // The calculation here is very simplified and is more accurate - // with higher frame-rate. - float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * - (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); - displayMagnitude[channelNr] = - std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); - } -} - -inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) -{ - QMutexLocker locker(&dataMutex); - - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) - calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); -} - -void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) -{ - QMutexLocker locker(&dataMutex); - QColor color; - - if (peakHold < minimumInputLevel) - color = backgroundNominalColor; - else if (peakHold < warningLevel) - color = foregroundNominalColor; - else if (peakHold < errorLevel) - color = foregroundWarningColor; - else if (peakHold <= clipLevel) - color = foregroundErrorColor; - else - color = clipColor; - - painter.fillRect(x, y, width, height, color); -} - -void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) -{ - qreal scale = width / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = int(x + width - (i * scale) - 1); - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - QRect textBounds = metrics.boundingRect(str); - int pos; - if (i == 0) { - pos = position - textBounds.width(); - } else { - pos = position - (textBounds.width() / 2); - if (pos < 0) - pos = 0; - } - painter.drawText(pos, y + 4 + metrics.capHeight(), str); - - painter.drawLine(position, y, position, y + 2); - } -} - -void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) -{ - qreal scale = height / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = y + int(i * scale) + METER_PADDING; - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - if (i == 0) { - painter.drawText(x + 10, position + metrics.capHeight(), str); - } else { - painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); - } - - painter.drawLine(x, position, x + 2, position); - } -} - -#define CLIP_FLASH_DURATION_MS 1000 - -inline int VolumeMeter::convertToInt(float number) -{ - constexpr int min = std::numeric_limits::min(); - constexpr int max = std::numeric_limits::max(); - - // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 - if (number >= (float)max) - return max; - else if (number < min) - return min; - else - return int(number); -} - -void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = width / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = x + 0; - int maximumPosition = x + width; - int magnitudePosition = x + width - convertToInt(magnitude * scale); - int peakPosition = x + width - convertToInt(peak * scale); - int peakHoldPosition = x + width - convertToInt(peakHold * scale); - int warningPosition = x + width - convertToInt(warningLevel * scale); - int errorPosition = x + width - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(minimumPosition, y, end, height, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); -} - -void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = height / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = y + 0; - int maximumPosition = y + height; - int magnitudePosition = y + height - convertToInt(magnitude * scale); - int peakPosition = y + height - convertToInt(peak * scale); - int peakHoldPosition = y + height - convertToInt(peakHold * scale); - int warningPosition = y + height - convertToInt(warningLevel * scale); - int errorPosition = y + height - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(x, minimumPosition, width, end, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); -} - -void VolumeMeter::paintEvent(QPaintEvent *event) -{ - uint64_t ts = os_gettime_ns(); - qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; - calculateBallistics(ts, timeSinceLastRedraw); - bool idle = detectIdle(ts); - - QRect widgetRect = rect(); - int width = widgetRect.width(); - int height = widgetRect.height(); - - QPainter painter(this); - - // Paint window background color (as widget is opaque) - QColor background = palette().color(QPalette::ColorRole::Window); - painter.fillRect(event->region().boundingRect(), background); - - if (vertical) - height -= METER_PADDING * 2; - - // timerEvent requests update of the bar(s) only, so we can avoid the - // overhead of repainting the scale and labels. - if (event->region().boundingRect() != getBarRect()) { - if (needLayoutChange()) - doLayout(); - - if (vertical) { - paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, - height - (INDICATOR_THICKNESS + 3)); - } else { - paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, - width - (INDICATOR_THICKNESS + 3)); - } - } - - if (vertical) { - // Invert the Y axis to ease the math - painter.translate(0, height + METER_PADDING); - painter.scale(1, -1); - } - - for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { - - int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; - - if (vertical) - paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, - height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - else - paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), - width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - - if (idle) - continue; - - // By not drawing the input meter boxes the user can - // see that the audio stream has been stopped, without - // having too much visual impact. - if (vertical) - paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, - INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); - else - paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, - meterThickness, displayInputPeakHold[channelNrFixed]); - } - - lastRedrawTime = ts; -} - -QRect VolumeMeter::getBarRect() const -{ - QRect rec = rect(); - if (vertical) - rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); - else - rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); - - return rec; -} - -void VolumeMeter::changeEvent(QEvent *e) -{ - if (e->type() == QEvent::StyleChange) - recalculateLayout = true; - - QWidget::changeEvent(e); -} - -void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) -{ - volumeMeters.push_back(meter); -} - -void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) -{ - volumeMeters.removeOne(meter); -} - -void VolumeMeterTimer::timerEvent(QTimerEvent *) -{ - for (VolumeMeter *meter : volumeMeters) { - if (meter->needLayoutChange()) { - // Tell paintEvent to update layout and paint everything - meter->update(); - } else { - // Tell paintEvent to paint only the bars - meter->update(meter->getBarRect()); - } - } -} +#include "moc_VolumeSlider.cpp" VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) { @@ -1447,61 +78,3 @@ void VolumeSlider::paintEvent(QPaintEvent *event) QSlider::paintEvent(event); } - -VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} - -VolumeSlider *VolumeAccessibleInterface::slider() const -{ - return qobject_cast(object()); -} - -QString VolumeAccessibleInterface::text(QAccessible::Text t) const -{ - if (slider()->isVisible()) { - switch (t) { - case QAccessible::Text::Value: - return currentValue().toString(); - default: - break; - } - } - return QAccessibleWidget::text(t); -} - -QVariant VolumeAccessibleInterface::currentValue() const -{ - QString text; - float db = obs_fader_get_db(slider()->fad); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - return text; -} - -void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) -{ - slider()->setValue(value.toInt()); -} - -QVariant VolumeAccessibleInterface::maximumValue() const -{ - return slider()->maximum(); -} - -QVariant VolumeAccessibleInterface::minimumValue() const -{ - return slider()->minimum(); -} - -QVariant VolumeAccessibleInterface::minimumStepSize() const -{ - return slider()->singleStep(); -} - -QAccessible::Role VolumeAccessibleInterface::role() const -{ - return QAccessible::Role::Slider; -} diff --git a/frontend/components/VolumeSlider.hpp b/frontend/components/VolumeSlider.hpp index 51c77f247..10b794889 100644 --- a/frontend/components/VolumeSlider.hpp +++ b/frontend/components/VolumeSlider.hpp @@ -1,303 +1,8 @@ #pragma once +#include + #include -#include -#include -#include -#include -#include -#include -#include -#include "absolute-slider.hpp" - -class QPushButton; -class VolumeMeterTimer; -class VolumeSlider; - -class VolumeMeter : public QWidget { - Q_OBJECT - Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) - - Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE - setBackgroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE - setBackgroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE - setBackgroundErrorColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE - setForegroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE - setForegroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE - setForegroundErrorColorDisabled DESIGNABLE true) - - Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) - Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) - Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) - Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) - Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) - Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) - - // Levels are denoted in dBFS. - Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) - Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) - Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) - Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) - Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) - - // Rates are denoted in dB/second. - Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) - - // Time in seconds for the VU meter to integrate over. - Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime - DESIGNABLE true) - - // Duration is denoted in seconds. - Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) - Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration - DESIGNABLE true) - - friend class VolControl; - -private: - obs_volmeter_t *obs_volmeter; - static std::weak_ptr updateTimer; - std::shared_ptr updateTimerRef; - - inline void resetLevels(); - inline void doLayout(); - inline bool detectIdle(uint64_t ts); - inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); - inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); - - inline int convertToInt(float number); - void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); - void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintHTicks(QPainter &painter, int x, int y, int width); - void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintVTicks(QPainter &painter, int x, int y, int height); - - QMutex dataMutex; - - bool recalculateLayout = true; - uint64_t currentLastUpdateTime = 0; - float currentMagnitude[MAX_AUDIO_CHANNELS]; - float currentPeak[MAX_AUDIO_CHANNELS]; - float currentInputPeak[MAX_AUDIO_CHANNELS]; - - int displayNrAudioChannels = 0; - float displayMagnitude[MAX_AUDIO_CHANNELS]; - float displayPeak[MAX_AUDIO_CHANNELS]; - float displayPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - float displayInputPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - - QFont tickFont; - QColor backgroundNominalColor; - QColor backgroundWarningColor; - QColor backgroundErrorColor; - QColor foregroundNominalColor; - QColor foregroundWarningColor; - QColor foregroundErrorColor; - - QColor backgroundNominalColorDisabled; - QColor backgroundWarningColorDisabled; - QColor backgroundErrorColorDisabled; - QColor foregroundNominalColorDisabled; - QColor foregroundWarningColorDisabled; - QColor foregroundErrorColorDisabled; - - QColor clipColor; - QColor magnitudeColor; - QColor majorTickColor; - QColor minorTickColor; - - int meterThickness; - qreal meterFontScaling; - - qreal minimumLevel; - qreal warningLevel; - qreal errorLevel; - qreal clipLevel; - qreal minimumInputLevel; - qreal peakDecayRate; - qreal magnitudeIntegrationTime; - qreal peakHoldDuration; - qreal inputPeakHoldDuration; - - QColor p_backgroundNominalColor; - QColor p_backgroundWarningColor; - QColor p_backgroundErrorColor; - QColor p_foregroundNominalColor; - QColor p_foregroundWarningColor; - QColor p_foregroundErrorColor; - - uint64_t lastRedrawTime = 0; - int channels = 0; - bool clipping = false; - bool vertical; - bool muted = false; - -public: - explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); - ~VolumeMeter(); - - void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]); - QRect getBarRect() const; - bool needLayoutChange(); - - QColor getBackgroundNominalColor() const; - void setBackgroundNominalColor(QColor c); - QColor getBackgroundWarningColor() const; - void setBackgroundWarningColor(QColor c); - QColor getBackgroundErrorColor() const; - void setBackgroundErrorColor(QColor c); - QColor getForegroundNominalColor() const; - void setForegroundNominalColor(QColor c); - QColor getForegroundWarningColor() const; - void setForegroundWarningColor(QColor c); - QColor getForegroundErrorColor() const; - void setForegroundErrorColor(QColor c); - - QColor getBackgroundNominalColorDisabled() const; - void setBackgroundNominalColorDisabled(QColor c); - QColor getBackgroundWarningColorDisabled() const; - void setBackgroundWarningColorDisabled(QColor c); - QColor getBackgroundErrorColorDisabled() const; - void setBackgroundErrorColorDisabled(QColor c); - QColor getForegroundNominalColorDisabled() const; - void setForegroundNominalColorDisabled(QColor c); - QColor getForegroundWarningColorDisabled() const; - void setForegroundWarningColorDisabled(QColor c); - QColor getForegroundErrorColorDisabled() const; - void setForegroundErrorColorDisabled(QColor c); - - QColor getClipColor() const; - void setClipColor(QColor c); - QColor getMagnitudeColor() const; - void setMagnitudeColor(QColor c); - QColor getMajorTickColor() const; - void setMajorTickColor(QColor c); - QColor getMinorTickColor() const; - void setMinorTickColor(QColor c); - int getMeterThickness() const; - void setMeterThickness(int v); - qreal getMeterFontScaling() const; - void setMeterFontScaling(qreal v); - qreal getMinimumLevel() const; - void setMinimumLevel(qreal v); - qreal getWarningLevel() const; - void setWarningLevel(qreal v); - qreal getErrorLevel() const; - void setErrorLevel(qreal v); - qreal getClipLevel() const; - void setClipLevel(qreal v); - qreal getMinimumInputLevel() const; - void setMinimumInputLevel(qreal v); - qreal getPeakDecayRate() const; - void setPeakDecayRate(qreal v); - qreal getMagnitudeIntegrationTime() const; - void setMagnitudeIntegrationTime(qreal v); - qreal getPeakHoldDuration() const; - void setPeakHoldDuration(qreal v); - qreal getInputPeakHoldDuration() const; - void setInputPeakHoldDuration(qreal v); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void wheelEvent(QWheelEvent *event) override; - -protected: - void paintEvent(QPaintEvent *event) override; - void changeEvent(QEvent *e) override; -}; - -class VolumeMeterTimer : public QTimer { - Q_OBJECT - -public: - inline VolumeMeterTimer() : QTimer() {} - - void AddVolControl(VolumeMeter *meter); - void RemoveVolControl(VolumeMeter *meter); - -protected: - void timerEvent(QTimerEvent *event) override; - QList volumeMeters; -}; - -class QLabel; -class VolumeSlider; -class MuteCheckBox; -class OBSSourceLabel; - -class VolControl : public QFrame { - Q_OBJECT - -private: - OBSSource source; - std::vector sigs; - OBSSourceLabel *nameLabel; - QLabel *volLabel; - VolumeMeter *volMeter; - VolumeSlider *slider; - MuteCheckBox *mute; - QPushButton *config = nullptr; - float levelTotal; - float levelCount; - OBSFader obs_fader; - OBSVolMeter obs_volmeter; - bool vertical; - QMenu *contextMenu; - - static void OBSVolumeChanged(void *param, float db); - static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); - static void OBSVolumeMuted(void *data, calldata_t *calldata); - static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); - - void EmitConfigClicked(); - -private slots: - void VolumeChanged(); - void VolumeMuted(bool muted); - void MixersOrMonitoringChanged(); - - void SetMuted(bool checked); - void SliderChanged(int vol); - void updateText(); - -signals: - void ConfigClicked(); - -public: - explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); - ~VolControl(); - - inline obs_source_t *GetSource() const { return source; } - - void SetMeterDecayRate(qreal q); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - - void EnableSlider(bool enable); - inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } - - void refreshColors(); -}; class VolumeSlider : public AbsoluteSlider { Q_OBJECT @@ -318,24 +23,3 @@ private: protected: virtual void paintEvent(QPaintEvent *event) override; }; - -class VolumeAccessibleInterface : public QAccessibleWidget { - -public: - VolumeAccessibleInterface(QWidget *w); - - QVariant currentValue() const; - void setCurrentValue(const QVariant &value); - - QVariant maximumValue() const; - QVariant minimumValue() const; - - QVariant minimumStepSize() const; - -private: - VolumeSlider *slider() const; - -protected: - virtual QAccessible::Role role() const override; - virtual QString text(QAccessible::Text t) const override; -}; diff --git a/frontend/utility/AdvancedOutput.cpp b/frontend/utility/AdvancedOutput.cpp index 30d203174..0d927721d 100644 --- a/frontend/utility/AdvancedOutput.cpp +++ b/frontend/utility/AdvancedOutput.cpp @@ -1,1453 +1,13 @@ -#include -#include -#include -#include -#include +#include "AdvancedOutput.hpp" + +#include +#include +#include + #include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" using namespace std; -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -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) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - static OBSData GetDataFromJsonFile(const char *jsonFile) { const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); @@ -1684,18 +244,6 @@ void AdvancedOutput::Update() UpdateAudioSettings(); } -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - inline bool AdvancedOutput::allowsMultiTrack() { const char *protocol = nullptr; @@ -2366,176 +914,3 @@ bool AdvancedOutput::ReplayBufferActive() const { return obs_output_active(replayBuffer); } - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/frontend/utility/AdvancedOutput.hpp b/frontend/utility/AdvancedOutput.hpp index 30d203174..a06191420 100644 --- a/frontend/utility/AdvancedOutput.hpp +++ b/frontend/utility/AdvancedOutput.hpp @@ -1,1408 +1,6 @@ -#include -#include -#include -#include -#include -#include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" +#pragma once -using namespace std; - -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -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) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ +#include "BasicOutputHandler.hpp" struct AdvancedOutput : BasicOutputHandler { OBSEncoder streamAudioEnc; @@ -1447,1095 +45,3 @@ struct AdvancedOutput : BasicOutputHandler { virtual bool ReplayBufferActive() const override; bool allowsMultiTrack(); }; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp index 30d203174..7488c4662 100644 --- a/frontend/utility/BasicOutputHandler.cpp +++ b/frontend/utility/BasicOutputHandler.cpp @@ -1,14 +1,15 @@ -#include -#include -#include -#include -#include +#include "BasicOutputHandler.hpp" +#include "AdvancedOutput.hpp" +#include "SimpleOutput.hpp" + +#include +#include +#include +#include + #include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" + +#include using namespace std; @@ -20,11 +21,7 @@ volatile bool recording_paused = false; volatile bool replaybuf_active = false; volatile bool virtualcam_active = false; -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -static void OBSStreamStarting(void *data, calldata_t *params) +void OBSStreamStarting(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); @@ -37,7 +34,7 @@ static void OBSStreamStarting(void *data, calldata_t *params) QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); } -static void OBSStreamStopping(void *data, calldata_t *params) +void OBSStreamStopping(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); @@ -49,7 +46,7 @@ static void OBSStreamStopping(void *data, calldata_t *params) QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); } -static void OBSStartStreaming(void *data, calldata_t * /* params */) +void OBSStartStreaming(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); output->streamingActive = true; @@ -57,7 +54,7 @@ static void OBSStartStreaming(void *data, calldata_t * /* params */) QMetaObject::invokeMethod(output->main, "StreamingStart"); } -static void OBSStopStreaming(void *data, calldata_t *params) +void OBSStopStreaming(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); int code = (int)calldata_int(params, "code"); @@ -72,7 +69,7 @@ static void OBSStopStreaming(void *data, calldata_t *params) QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); } -static void OBSStartRecording(void *data, calldata_t * /* params */) +void OBSStartRecording(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); @@ -81,7 +78,7 @@ static void OBSStartRecording(void *data, calldata_t * /* params */) QMetaObject::invokeMethod(output->main, "RecordingStart"); } -static void OBSStopRecording(void *data, calldata_t *params) +void OBSStopRecording(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); int code = (int)calldata_int(params, "code"); @@ -95,13 +92,13 @@ static void OBSStopRecording(void *data, calldata_t *params) QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); } -static void OBSRecordStopping(void *data, calldata_t * /* params */) +void OBSRecordStopping(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); QMetaObject::invokeMethod(output->main, "RecordStopping"); } -static void OBSRecordFileChanged(void *data, calldata_t *params) +void OBSRecordFileChanged(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); const char *next_file = calldata_string(params, "next_file"); @@ -113,7 +110,7 @@ static void OBSRecordFileChanged(void *data, calldata_t *params) output->lastRecordingPath = next_file; } -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +void OBSStartReplayBuffer(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); @@ -122,7 +119,7 @@ static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); } -static void OBSStopReplayBuffer(void *data, calldata_t *params) +void OBSStopReplayBuffer(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); int code = (int)calldata_int(params, "code"); @@ -132,13 +129,13 @@ static void OBSStopReplayBuffer(void *data, calldata_t *params) QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); } -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +void OBSReplayBufferStopping(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); } -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +void OBSReplayBufferSaved(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); @@ -169,71 +166,7 @@ static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) output->DestroyVirtualCamView(); } -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) +bool return_first_id(void *data, const char *id) { const char **output = (const char **)data; @@ -241,7 +174,7 @@ static bool return_first_id(void *data, const char *id) return false; } -static const char *GetStreamOutputType(const obs_service_t *service) +const char *GetStreamOutputType(const obs_service_t *service) { const char *protocol = obs_service_get_protocol(service); const char *output = nullptr; @@ -284,9 +217,7 @@ static const char *GetStreamOutputType(const obs_service_t *service) return nullptr; } -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) { if (main->vcamEnabled) { virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); @@ -450,581 +381,6 @@ void BasicOutputHandler::DestroyVirtualCameraScene() vCamSourceSceneItem = nullptr; } -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - const char *FindAudioEncoderFromCodec(const char *type) { const char *alt_enc_id = nullptr; @@ -1040,74 +396,7 @@ const char *FindAudioEncoderFromCodec(const char *type) return nullptr; } -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +void clear_archive_encoder(obs_output_t *output, const char *expected_name) { obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); bool clear = false; @@ -1123,1252 +412,6 @@ static void clear_archive_encoder(obs_output_t *output, const char *expected_nam obs_output_set_audio_encoder(output, nullptr, 1); } -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - void BasicOutputHandler::SetupAutoRemux(const char *&container) { bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); diff --git a/frontend/utility/BasicOutputHandler.hpp b/frontend/utility/BasicOutputHandler.hpp index f7178f19d..7d041304a 100644 --- a/frontend/utility/BasicOutputHandler.hpp +++ b/frontend/utility/BasicOutputHandler.hpp @@ -1,10 +1,15 @@ #pragma once -#include -#include -#include +#include -#include "multitrack-video-output.hpp" +#include +#include + +#include + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" class OBSBasic; @@ -58,7 +63,7 @@ struct BasicOutputHandler { OBSSignal replayBufferStopping; OBSSignal replayBufferSaved; - inline BasicOutputHandler(OBSBasic *main_); + BasicOutputHandler(OBSBasic *main_); virtual ~BasicOutputHandler(){}; @@ -103,3 +108,39 @@ protected: BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main); BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main); + +void OBSStreamStarting(void *data, calldata_t *params); +void OBSStreamStopping(void *data, calldata_t *params); +void OBSStartStreaming(void *data, calldata_t *params); +void OBSStopStreaming(void *data, calldata_t *params); +void OBSStartRecording(void *data, calldata_t *params); +void OBSStopRecording(void *data, calldata_t *params); +void OBSRecordStopping(void *data, calldata_t *params); +void OBSRecordFileChanged(void *data, calldata_t *params); +void OBSStartReplayBuffer(void *data, calldata_t *params); +void OBSStopReplayBuffer(void *data, calldata_t *params); +void OBSReplayBufferStopping(void *data, calldata_t *params); +void OBSReplayBufferSaved(void *data, calldata_t *params); + +inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +const char *GetStreamOutputType(const obs_service_t *service); + +inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +void clear_archive_encoder(obs_output_t *output, const char *expected_name); diff --git a/frontend/utility/QuickTransition.cpp b/frontend/utility/QuickTransition.cpp index a17137b2b..f0179a6cd 100644 --- a/frontend/utility/QuickTransition.cpp +++ b/frontend/utility/QuickTransition.cpp @@ -15,27 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include +#include "QuickTransition.hpp" + +#include + #include -#include -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "display-helpers.hpp" -#include "window-namedialog.hpp" -#include "menu-button.hpp" - -#include "obs-hotkey.h" - -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(QuickTransition); static inline QString MakeQuickTransitionText(QuickTransition *qt) { @@ -51,54 +35,6 @@ static inline QString MakeQuickTransitionText(QuickTransition *qt) return name; } -void OBSBasic::InitDefaultTransitions() -{ - std::vector transitions; - size_t idx = 0; - const char *id; - - /* automatically add transitions that have no configuration (things - * such as cut/fade/etc) */ - while (obs_enum_transition_types(idx++, &id)) { - if (!obs_is_source_configurable(id)) { - const char *name = obs_source_get_display_name(id); - - OBSSourceAutoRelease tr = obs_source_create_private(id, name, NULL); - InitTransition(tr); - transitions.emplace_back(tr); - - if (strcmp(id, "fade_transition") == 0) - fadeTransition = tr; - else if (strcmp(id, "cut_transition") == 0) - cutTransition = tr; - } - } - - for (OBSSource &tr : transitions) { - ui->transitions->addItem(QT_UTF8(obs_source_get_name(tr)), QVariant::fromValue(OBSSource(tr))); - } -} - -void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) -{ - DStr hotkeyId; - QString hotkeyName; - - dstr_printf(hotkeyId, "OBSBasic.QuickTransition.%d", qt->id); - hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); - - auto quickTransition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - int id = (int)(uintptr_t)data; - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (pressed) - QMetaObject::invokeMethod(main, "TriggerQuickTransition", Qt::QueuedConnection, Q_ARG(int, id)); - }; - - qt->hotkey = obs_hotkey_register_frontend(hotkeyId->array, QT_TO_UTF8(hotkeyName), quickTransition, - (void *)(uintptr_t)qt->id); -} - void QuickTransition::SourceRenamed(void *param, calldata_t *) { QuickTransition *qt = reinterpret_cast(param); @@ -107,1593 +43,3 @@ void QuickTransition::SourceRenamed(void *param, calldata_t *) obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); } - -void OBSBasic::TriggerQuickTransition(int id) -{ - QuickTransition *qt = GetQuickTransition(id); - - if (qt && previewProgramMode) { - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (GetCurrentTransition() != qt->source) { - OverrideTransition(qt->source); - overridingTransition = true; - } - - TransitionToScene(source, false, true, qt->duration, qt->fadeToBlack); - } -} - -void OBSBasic::RemoveQuickTransitionHotkey(QuickTransition *qt) -{ - obs_hotkey_unregister(qt->hotkey); -} - -void OBSBasic::InitTransition(obs_source_t *transition) -{ - auto onTransitionStop = [](void *data, calldata_t *) { - OBSBasic *window = (OBSBasic *)data; - QMetaObject::invokeMethod(window, "TransitionStopped", Qt::QueuedConnection); - }; - - auto onTransitionFullStop = [](void *data, calldata_t *) { - OBSBasic *window = (OBSBasic *)data; - QMetaObject::invokeMethod(window, "TransitionFullyStopped", Qt::QueuedConnection); - }; - - signal_handler_t *handler = obs_source_get_signal_handler(transition); - signal_handler_connect(handler, "transition_video_stop", onTransitionStop, this); - signal_handler_connect(handler, "transition_stop", onTransitionFullStop, this); -} - -static inline OBSSource GetTransitionComboItem(QComboBox *combo, int idx) -{ - return combo->itemData(idx).value(); -} - -void OBSBasic::CreateDefaultQuickTransitions() -{ - /* non-configurable transitions are always available, so add them - * to the "default quick transitions" list */ - quickTransitions.emplace_back(cutTransition, 300, quickTransitionIdCounter++); - quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++); - quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++, true); -} - -void OBSBasic::LoadQuickTransitions(obs_data_array_t *array) -{ - size_t count = obs_data_array_count(array); - - quickTransitionIdCounter = 1; - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - OBSDataArrayAutoRelease hotkeys = obs_data_get_array(data, "hotkeys"); - const char *name = obs_data_get_string(data, "name"); - int duration = obs_data_get_int(data, "duration"); - int id = obs_data_get_int(data, "id"); - bool toBlack = obs_data_get_bool(data, "fade_to_black"); - - if (id) { - obs_source_t *source = FindTransition(name); - if (source) { - quickTransitions.emplace_back(source, duration, id, toBlack); - - if (quickTransitionIdCounter <= id) - quickTransitionIdCounter = id + 1; - - int idx = (int)quickTransitions.size() - 1; - AddQuickTransitionHotkey(&quickTransitions[idx]); - obs_hotkey_load(quickTransitions[idx].hotkey, hotkeys); - } - } - } -} - -obs_data_array_t *OBSBasic::SaveQuickTransitions() -{ - obs_data_array_t *array = obs_data_array_create(); - - for (QuickTransition &qt : quickTransitions) { - OBSDataAutoRelease data = obs_data_create(); - OBSDataArrayAutoRelease hotkeys = obs_hotkey_save(qt.hotkey); - - obs_data_set_string(data, "name", obs_source_get_name(qt.source)); - obs_data_set_int(data, "duration", qt.duration); - obs_data_set_array(data, "hotkeys", hotkeys); - obs_data_set_int(data, "id", qt.id); - obs_data_set_bool(data, "fade_to_black", qt.fadeToBlack); - - obs_data_array_push_back(array, data); - } - - return array; -} - -obs_source_t *OBSBasic::FindTransition(const char *name) -{ - for (int i = 0; i < ui->transitions->count(); i++) { - OBSSource tr = ui->transitions->itemData(i).value(); - if (!tr) - continue; - - const char *trName = obs_source_get_name(tr); - if (strcmp(trName, name) == 0) - return tr; - } - - return nullptr; -} - -void OBSBasic::TransitionToScene(OBSScene scene, bool force) -{ - obs_source_t *source = obs_scene_get_source(scene); - TransitionToScene(source, force); -} - -void OBSBasic::TransitionStopped() -{ - if (swapScenesMode) { - OBSSource scene = OBSGetStrongRef(swapScene); - if (scene) - SetCurrentScene(scene); - } - - EnableTransitionWidgets(true); - UpdatePreviewProgramIndicators(); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_STOPPED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - - swapScene = nullptr; -} - -void OBSBasic::OverrideTransition(OBSSource transition) -{ - OBSSourceAutoRelease oldTransition = obs_get_output_source(0); - - if (transition != oldTransition) { - obs_transition_swap_begin(transition, oldTransition); - obs_set_output_source(0, transition); - obs_transition_swap_end(transition, oldTransition); - } -} - -void OBSBasic::TransitionFullyStopped() -{ - if (overridingTransition) { - OverrideTransition(GetCurrentTransition()); - overridingTransition = false; - } -} - -void OBSBasic::TransitionToScene(OBSSource source, bool force, bool quickTransition, int quickDuration, bool black, - bool manual) -{ - obs_scene_t *scene = obs_scene_from_source(source); - bool usingPreviewProgram = IsPreviewProgramMode(); - if (!scene) - return; - - if (usingPreviewProgram) { - if (!tBarActive) - lastProgramScene = programScene; - programScene = OBSGetWeakRef(source); - - if (!force && !black) { - OBSSource lastScene = OBSGetStrongRef(lastProgramScene); - - if (!sceneDuplicationMode && lastScene == source) - return; - - if (swapScenesMode && lastScene && lastScene != GetCurrentSceneSource()) - swapScene = lastProgramScene; - } - } - - if (usingPreviewProgram && sceneDuplicationMode) { - scene = obs_scene_duplicate(scene, obs_source_get_name(obs_scene_get_source(scene)), - editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY - : OBS_SCENE_DUP_PRIVATE_REFS); - source = obs_scene_get_source(scene); - } - - OBSSourceAutoRelease transition = obs_get_output_source(0); - if (!transition) { - if (usingPreviewProgram && sceneDuplicationMode) - obs_scene_release(scene); - return; - } - - float t = obs_transition_get_time(transition); - bool stillTransitioning = t < 1.0f && t > 0.0f; - - // If actively transitioning, block new transitions from starting - if (usingPreviewProgram && stillTransitioning) - goto cleanup; - - if (usingPreviewProgram) { - if (!black && !manual) { - const char *sceneName = obs_source_get_name(source); - blog(LOG_INFO, "User switched Program to scene '%s'", sceneName); - - } else if (black && !prevFTBSource) { - OBSSourceAutoRelease target = obs_transition_get_active_source(transition); - const char *sceneName = obs_source_get_name(target); - blog(LOG_INFO, "User faded from scene '%s' to black", sceneName); - - } else if (black && prevFTBSource) { - const char *sceneName = obs_source_get_name(prevFTBSource); - blog(LOG_INFO, "User faded from black to scene '%s'", sceneName); - - } else if (manual) { - const char *sceneName = obs_source_get_name(source); - blog(LOG_INFO, "User started manual transition to scene '%s'", sceneName); - } - } - - if (force) { - obs_transition_set(transition, source); - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - } else { - int duration = ui->transitionDuration->value(); - - /* check for scene override */ - OBSSource trOverride = GetOverrideTransition(source); - - if (trOverride && !overridingTransition && !quickTransition) { - transition = std::move(trOverride); - duration = GetOverrideTransitionDuration(source); - OverrideTransition(transition.Get()); - overridingTransition = true; - } - - if (black && !prevFTBSource) { - prevFTBSource = source; - source = nullptr; - } else if (black && prevFTBSource) { - source = prevFTBSource; - prevFTBSource = nullptr; - } else if (!black) { - prevFTBSource = nullptr; - } - - if (quickTransition) - duration = quickDuration; - - enum obs_transition_mode mode = manual ? OBS_TRANSITION_MODE_MANUAL : OBS_TRANSITION_MODE_AUTO; - - EnableTransitionWidgets(false); - - bool success = obs_transition_start(transition, mode, duration, source); - - if (!success) - TransitionFullyStopped(); - } - -cleanup: - if (usingPreviewProgram && sceneDuplicationMode) - obs_scene_release(scene); -} - -static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr) -{ - int idx = combo->findData(QVariant::fromValue(tr)); - if (idx != -1) { - combo->blockSignals(true); - combo->setCurrentIndex(idx); - combo->blockSignals(false); - } -} - -void OBSBasic::SetTransition(OBSSource transition) -{ - OBSSourceAutoRelease oldTransition = obs_get_output_source(0); - - if (oldTransition && transition) { - obs_transition_swap_begin(transition, oldTransition); - if (transition != GetCurrentTransition()) - SetComboTransition(ui->transitions, transition); - obs_set_output_source(0, transition); - obs_transition_swap_end(transition, oldTransition); - } else { - obs_set_output_source(0, transition); - } - - bool fixed = transition ? obs_transition_fixed(transition) : false; - ui->transitionDurationLabel->setVisible(!fixed); - ui->transitionDuration->setVisible(!fixed); - - bool configurable = transition ? obs_source_configurable(transition) : false; - ui->transitionRemove->setEnabled(configurable); - ui->transitionProps->setEnabled(configurable); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_CHANGED); -} - -OBSSource OBSBasic::GetCurrentTransition() -{ - return ui->transitions->currentData().value(); -} - -void OBSBasic::on_transitions_currentIndexChanged(int) -{ - OBSSource transition = GetCurrentTransition(); - SetTransition(transition); -} - -void OBSBasic::AddTransition(const char *id) -{ - string name; - QString placeHolderText = QT_UTF8(obs_source_get_display_name(id)); - QString format = placeHolderText + " (%1)"; - obs_source_t *source = nullptr; - int i = 1; - - while ((FindTransition(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), - name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - AddTransition(id); - return; - } - - source = FindTransition(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - AddTransition(id); - return; - } - - source = obs_source_create_private(id, name.c_str(), NULL); - InitTransition(source); - ui->transitions->addItem(QT_UTF8(name.c_str()), QVariant::fromValue(OBSSource(source))); - ui->transitions->setCurrentIndex(ui->transitions->count() - 1); - CreatePropertiesWindow(source); - obs_source_release(source); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); - } -} - -void OBSBasic::on_transitionAdd_clicked() -{ - bool foundConfigurableTransitions = false; - QMenu menu(this); - size_t idx = 0; - const char *id; - - while (obs_enum_transition_types(idx++, &id)) { - if (obs_is_source_configurable(id)) { - const char *name = obs_source_get_display_name(id); - QAction *action = new QAction(name, this); - - connect(action, &QAction::triggered, [this, id]() { AddTransition(id); }); - - menu.addAction(action); - foundConfigurableTransitions = true; - } - } - - if (foundConfigurableTransitions) - menu.exec(QCursor::pos()); -} - -void OBSBasic::on_transitionRemove_clicked() -{ - OBSSource tr = GetCurrentTransition(); - - if (!tr || !obs_source_configurable(tr) || !QueryRemoveSource(tr)) - return; - - int idx = ui->transitions->findData(QVariant::fromValue(tr)); - if (idx == -1) - return; - - for (size_t i = quickTransitions.size(); i > 0; i--) { - QuickTransition &qt = quickTransitions[i - 1]; - if (qt.source == tr) { - if (qt.button) - qt.button->deleteLater(); - RemoveQuickTransitionHotkey(&qt); - quickTransitions.erase(quickTransitions.begin() + i - 1); - } - } - - ui->transitions->removeItem(idx); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); -} - -void OBSBasic::RenameTransition(OBSSource transition) -{ - string name; - QString placeHolderText = QT_UTF8(obs_source_get_name(transition)); - obs_source_t *source = nullptr; - - bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), - name, placeHolderText); - - if (!accepted) - return; - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - RenameTransition(transition); - return; - } - - source = FindTransition(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - RenameTransition(transition); - return; - } - - obs_source_set_name(transition, name.c_str()); - int idx = ui->transitions->findData(QVariant::fromValue(transition)); - if (idx != -1) { - ui->transitions->setItemText(idx, QT_UTF8(name.c_str())); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); - } -} - -void OBSBasic::on_transitionProps_clicked() -{ - OBSSource source = GetCurrentTransition(); - - if (!obs_source_configurable(source)) - return; - - auto properties = [&]() { - CreatePropertiesWindow(source); - }; - - QMenu menu(this); - - QAction *action = new QAction(QTStr("Rename"), &menu); - connect(action, &QAction::triggered, [this, source]() { RenameTransition(source); }); - menu.addAction(action); - - action = new QAction(QTStr("Properties"), &menu); - connect(action, &QAction::triggered, properties); - menu.addAction(action); - - menu.exec(QCursor::pos()); -} - -void OBSBasic::on_transitionDuration_valueChanged() -{ - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_DURATION_CHANGED); -} - -QuickTransition *OBSBasic::GetQuickTransition(int id) -{ - for (QuickTransition &qt : quickTransitions) { - if (qt.id == id) - return &qt; - } - - return nullptr; -} - -int OBSBasic::GetQuickTransitionIdx(int id) -{ - for (int idx = 0; idx < (int)quickTransitions.size(); idx++) { - QuickTransition &qt = quickTransitions[idx]; - - if (qt.id == id) - return idx; - } - - return -1; -} - -void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force) -{ - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, force); -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -void OBSBasic::SetCurrentScene(OBSSource scene, bool force) -{ - if (!IsPreviewProgramMode()) { - TransitionToScene(scene, force); - } else { - OBSSource actualLastScene = OBSGetStrongRef(lastScene); - if (actualLastScene != scene) { - if (scene) - obs_source_inc_showing(scene); - if (actualLastScene) - obs_source_dec_showing(actualLastScene); - lastScene = OBSGetWeakRef(scene); - } - } - - if (obs_scene_get_source(GetCurrentScene()) != scene) { - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene itemScene = GetOBSRef(item); - obs_source_t *source = obs_scene_get_source(itemScene); - - if (source == scene) { - ui->scenes->blockSignals(true); - currentScene = itemScene.Get(); - ui->scenes->setCurrentItem(item); - ui->scenes->blockSignals(false); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - break; - } - } - } - - UpdateContextBar(true); - UpdatePreviewProgramIndicators(); - - if (scene) { - bool userSwitched = (!force && !disableSaving); - blog(LOG_INFO, "%s to scene '%s'", userSwitched ? "User switched" : "Switched", - obs_source_get_name(scene)); - } -} - -void OBSBasic::CreateProgramDisplay() -{ - program = new OBSQTDisplay(); - - program->setContextMenuPolicy(Qt::CustomContextMenu); - connect(program.data(), &QWidget::customContextMenuRequested, this, &OBSBasic::ProgramViewContextMenuRequested); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayResized, displayResize); - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderProgram, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayCreated, addDisplay); - - program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -} - -void OBSBasic::TransitionClicked() -{ - if (previewProgramMode) - TransitionToScene(GetCurrentScene()); -} - -#define T_BAR_PRECISION 1024 -#define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) -#define T_BAR_CLAMP (T_BAR_PRECISION / 10) - -void OBSBasic::CreateProgramOptions() -{ - programOptions = new QWidget(); - QVBoxLayout *layout = new QVBoxLayout(); - layout->setSpacing(4); - - QPushButton *configTransitions = new QPushButton(); - configTransitions->setProperty("class", "icon-dots-vert"); - - QHBoxLayout *mainButtonLayout = new QHBoxLayout(); - mainButtonLayout->setSpacing(2); - - transitionButton = new QPushButton(QTStr("Transition")); - transitionButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - QHBoxLayout *quickTransitions = new QHBoxLayout(); - quickTransitions->setSpacing(2); - - QPushButton *addQuickTransition = new QPushButton(); - addQuickTransition->setProperty("class", "icon-plus"); - - QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions")); - quickTransitionsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - quickTransitions->addWidget(quickTransitionsLabel); - quickTransitions->addWidget(addQuickTransition); - - mainButtonLayout->addWidget(transitionButton); - mainButtonLayout->addWidget(configTransitions); - - tBar = new SliderIgnoreClick(Qt::Horizontal); - tBar->setMinimum(0); - tBar->setMaximum(T_BAR_PRECISION - 1); - - tBar->setProperty("class", "slider-tbar"); - - connect(tBar, &QSlider::valueChanged, this, &OBSBasic::TBarChanged); - connect(tBar, &QSlider::sliderReleased, this, &OBSBasic::TBarReleased); - - layout->addStretch(0); - layout->addLayout(mainButtonLayout); - layout->addLayout(quickTransitions); - layout->addWidget(tBar); - layout->addStretch(0); - - programOptions->setLayout(layout); - - auto onAdd = [this]() { - QScopedPointer menu(CreateTransitionMenu(this, nullptr)); - menu->exec(QCursor::pos()); - }; - - auto onConfig = [this]() { - QMenu menu(this); - QAction *action; - - auto toggleEditProperties = [this]() { - editPropertiesMode = !editPropertiesMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto toggleSwapScenesMode = [this]() { - swapScenesMode = !swapScenesMode; - }; - - auto toggleSceneDuplication = [this]() { - sceneDuplicationMode = !sceneDuplicationMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto showToolTip = [&]() { - QAction *act = menu.activeAction(); - QToolTip::showText(QCursor::pos(), act->toolTip(), &menu, menu.actionGeometry(act)); - }; - - action = menu.addAction(QTStr("QuickTransitions.DuplicateScene")); - action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT")); - action->setCheckable(true); - action->setChecked(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleSceneDuplication); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.EditProperties")); - action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT")); - action->setCheckable(true); - action->setChecked(editPropertiesMode); - action->setEnabled(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleEditProperties); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.SwapScenes")); - action->setToolTip(QTStr("QuickTransitions.SwapScenesTT")); - action->setCheckable(true); - action->setChecked(swapScenesMode); - connect(action, &QAction::triggered, toggleSwapScenesMode); - connect(action, &QAction::hovered, showToolTip); - - menu.exec(QCursor::pos()); - }; - - connect(transitionButton.data(), &QAbstractButton::clicked, this, &OBSBasic::TransitionClicked); - connect(addQuickTransition, &QAbstractButton::clicked, onAdd); - connect(configTransitions, &QAbstractButton::clicked, onConfig); -} - -void OBSBasic::TBarReleased() -{ - int val = tBar->value(); - - OBSSourceAutoRelease transition = obs_get_output_source(0); - - if ((tBar->maximum() - val) <= T_BAR_CLAMP) { - obs_transition_set_manual_time(transition, 1.0f); - tBar->blockSignals(true); - tBar->setValue(0); - tBar->blockSignals(false); - tBarActive = false; - EnableTransitionWidgets(true); - - OBSSourceAutoRelease target = obs_transition_get_active_source(transition); - const char *sceneName = obs_source_get_name(target); - blog(LOG_INFO, "Manual transition to scene '%s' finished", sceneName); - } else if (val <= T_BAR_CLAMP) { - obs_transition_set_manual_time(transition, 0.0f); - TransitionFullyStopped(); - tBar->blockSignals(true); - tBar->setValue(0); - tBar->blockSignals(false); - tBarActive = false; - EnableTransitionWidgets(true); - programScene = lastProgramScene; - blog(LOG_INFO, "Manual transition cancelled"); - } - - tBar->clearFocus(); -} - -static bool ValidTBarTransition(OBSSource transition) -{ - if (!transition) - return false; - - QString id = QT_UTF8(obs_source_get_id(transition)); - - if (id == "cut_transition" || id == "obs_stinger_transition") - return false; - - return true; -} - -void OBSBasic::TBarChanged(int value) -{ - OBSSourceAutoRelease transition = obs_get_output_source(0); - - if (!tBarActive) { - OBSSource sceneSource = GetCurrentSceneSource(); - OBSSource tBarTr = GetOverrideTransition(sceneSource); - - if (!ValidTBarTransition(tBarTr)) { - tBarTr = GetCurrentTransition(); - - if (!ValidTBarTransition(tBarTr)) - tBarTr = FindTransition(obs_source_get_display_name("fade_transition")); - - OverrideTransition(tBarTr); - overridingTransition = true; - - transition = std::move(tBarTr); - } - - obs_transition_set_manual_torque(transition, 8.0f, 0.05f); - TransitionToScene(sceneSource, false, false, false, 0, true); - tBarActive = true; - } - - obs_transition_set_manual_time(transition, (float)value / T_BAR_PRECISION_F); - - OnEvent(OBS_FRONTEND_EVENT_TBAR_VALUE_CHANGED); -} - -int OBSBasic::GetTbarPosition() -{ - return tBar->value(); -} - -void OBSBasic::TogglePreviewProgramMode() -{ - SetPreviewProgramMode(!IsPreviewProgramMode()); -} - -static inline void ResetQuickTransitionText(QuickTransition *qt) -{ - qt->button->setText(MakeQuickTransitionText(qt)); -} - -QMenu *OBSBasic::CreatePerSceneTransitionMenu() -{ - OBSSource scene = GetCurrentSceneSource(); - QMenu *menu = new QMenu(QTStr("TransitionOverride")); - QAction *action; - - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - obs_data_set_default_int(data, "transition_duration", 300); - - const char *curTransition = obs_data_get_string(data, "transition"); - int curDuration = (int)obs_data_get_int(data, "transition_duration"); - - QSpinBox *duration = new QSpinBox(menu); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(curDuration); - - auto setTransition = [this](QAction *action) { - int idx = action->property("transition_index").toInt(); - OBSSource scene = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - if (idx == -1) { - obs_data_set_string(data, "transition", ""); - return; - } - - OBSSource tr = GetTransitionComboItem(ui->transitions, idx); - - if (tr) { - const char *name = obs_source_get_name(tr); - obs_data_set_string(data, "transition", name); - } - }; - - auto setDuration = [this](int duration) { - OBSSource scene = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - obs_data_set_int(data, "transition_duration", duration); - }; - - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); - - for (int i = -1; i < ui->transitions->count(); i++) { - const char *name = ""; - - if (i >= 0) { - OBSSource tr; - tr = GetTransitionComboItem(ui->transitions, i); - if (!tr) - continue; - name = obs_source_get_name(tr); - } - - bool match = (name && strcmp(name, curTransition) == 0); - - if (!name || !*name) - name = Str("None"); - - action = menu->addAction(QT_UTF8(name)); - action->setProperty("transition_index", i); - action->setCheckable(true); - action->setChecked(match); - - connect(action, &QAction::triggered, std::bind(setTransition, action)); - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - return menu; -} - -void OBSBasic::ShowTransitionProperties() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_transition(item, true); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::HideTransitionProperties() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_transition(item, false); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration) -{ - int64_t sceneItemId = obs_sceneitem_get_id(item); - std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - auto undo_redo = [sceneUUID, sceneItemId, show](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); - if (i) { - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - obs_sceneitem_transition_load(i, dat, show); - } - }; - - OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(item, show); - - OBSSourceAutoRelease dup = obs_source_duplicate(tr, obs_source_get_name(tr), true); - obs_sceneitem_set_transition(item, show, dup); - obs_sceneitem_set_transition_duration(item, show, duration); - - OBSDataAutoRelease transitionData = obs_sceneitem_transition_save(item, show); - - std::string undo_data(obs_data_get_json(oldTransitionData)); - std::string redo_data(obs_data_get_json(transitionData)); - if (undo_data.compare(redo_data) == 0) - return; - - QString text = show ? QTStr("Undo.ShowTransition") : QTStr("Undo.HideTransition"); - const char *name = obs_source_get_name(obs_sceneitem_get_source(item)); - undo_s.add_action(text.arg(name), undo_redo, undo_redo, undo_data, redo_data); -} - -QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) -{ - OBSSceneItem si = GetCurrentSceneItem(); - - QMenu *menu = new QMenu(QTStr(visible ? "ShowTransition" : "HideTransition")); - QAction *action; - - OBSSource curTransition = obs_sceneitem_get_transition(si, visible); - const char *curId = curTransition ? obs_source_get_id(curTransition) : nullptr; - int curDuration = (int)obs_sceneitem_get_transition_duration(si, visible); - - if (curDuration <= 0) - curDuration = obs_frontend_get_transition_duration(); - - QSpinBox *duration = new QSpinBox(menu); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(curDuration); - - auto setTransition = [this](QAction *action, bool visible) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - QString id = action->property("transition_id").toString(); - OBSSceneItem sceneItem = main->GetCurrentSceneItem(); - int64_t sceneItemId = obs_sceneitem_get_id(sceneItem); - std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(sceneItem))); - - auto undo_redo = [sceneUUID, sceneItemId, visible](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); - if (i) { - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - obs_sceneitem_transition_load(i, dat, visible); - } - }; - OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(sceneItem, visible); - if (id.isNull() || id.isEmpty()) { - obs_sceneitem_set_transition(sceneItem, visible, nullptr); - obs_sceneitem_set_transition_duration(sceneItem, visible, 0); - } else { - OBSSource tr = obs_sceneitem_get_transition(sceneItem, visible); - - if (!tr || strcmp(QT_TO_UTF8(id), obs_source_get_id(tr)) != 0) { - QString name = QT_UTF8(obs_source_get_name(obs_sceneitem_get_source(sceneItem))); - name += " "; - name += QTStr(visible ? "ShowTransition" : "HideTransition"); - tr = obs_source_create_private(QT_TO_UTF8(id), QT_TO_UTF8(name), nullptr); - obs_sceneitem_set_transition(sceneItem, visible, tr); - obs_source_release(tr); - - int duration = (int)obs_sceneitem_get_transition_duration(sceneItem, visible); - if (duration <= 0) { - duration = obs_frontend_get_transition_duration(); - obs_sceneitem_set_transition_duration(sceneItem, visible, duration); - } - } - if (obs_source_configurable(tr)) - CreatePropertiesWindow(tr); - } - OBSDataAutoRelease newTransitionData = obs_sceneitem_transition_save(sceneItem, visible); - std::string undo_data(obs_data_get_json(oldTransitionData)); - std::string redo_data(obs_data_get_json(newTransitionData)); - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr(visible ? "Undo.ShowTransition" : "Undo.HideTransition") - .arg(obs_source_get_name(obs_sceneitem_get_source(sceneItem))), - undo_redo, undo_redo, undo_data, redo_data); - }; - auto setDuration = [visible](int duration) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSceneItem item = main->GetCurrentSceneItem(); - obs_sceneitem_set_transition_duration(item, visible, duration); - }; - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); - - action = menu->addAction(QT_UTF8(Str("None"))); - action->setProperty("transition_id", QT_UTF8("")); - action->setCheckable(true); - action->setChecked(!curId); - connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); - size_t idx = 0; - const char *id; - while (obs_enum_transition_types(idx++, &id)) { - const char *name = obs_source_get_display_name(id); - const bool match = id && curId && strcmp(id, curId) == 0; - action = menu->addAction(QT_UTF8(name)); - action->setProperty("transition_id", QT_UTF8(id)); - action->setCheckable(true); - action->setChecked(match); - connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - if (curId && obs_is_source_configurable(curId)) { - menu->addSeparator(); - menu->addAction(QTStr("Properties"), this, - visible ? &OBSBasic::ShowTransitionProperties : &OBSBasic::HideTransitionProperties); - } - - auto copyTransition = [this](QAction *, bool visible) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSceneItem item = main->GetCurrentSceneItem(); - obs_source_t *tr = obs_sceneitem_get_transition(item, visible); - int trDur = obs_sceneitem_get_transition_duration(item, visible); - main->copySourceTransition = obs_source_get_weak_source(tr); - main->copySourceTransitionDuration = trDur; - }; - menu->addSeparator(); - action = menu->addAction(QT_UTF8(Str("Copy"))); - action->setEnabled(curId != nullptr); - connect(action, &QAction::triggered, std::bind(copyTransition, action, visible)); - - auto pasteTransition = [this](QAction *, bool show) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSource tr = OBSGetStrongRef(main->copySourceTransition); - int trDuration = main->copySourceTransitionDuration; - if (!tr) - return; - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = main->ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - PasteShowHideTransition(item, show, tr, trDuration); - } - }; - - action = menu->addAction(QT_UTF8(Str("Paste"))); - action->setEnabled(!!OBSGetStrongRef(copySourceTransition)); - connect(action, &QAction::triggered, std::bind(pasteTransition, action, visible)); - return menu; -} - -QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt) -{ - QMenu *menu = new QMenu(parent); - QAction *action; - OBSSource tr; - - if (qt) { - action = menu->addAction(QTStr("Remove")); - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionRemoveClicked); - - menu->addSeparator(); - } - - QSpinBox *duration = new QSpinBox(menu); - if (qt) - duration->setProperty("id", qt->id); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(qt ? qt->duration : 300); - - if (qt) { - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, this, - &OBSBasic::QuickTransitionChangeDuration); - } - - tr = fadeTransition; - - action = menu->addAction(QTStr("FadeToBlack")); - action->setProperty("fadeToBlack", true); - - if (qt) { - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); - } else { - action->setProperty("duration", QVariant::fromValue(duration)); - connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); - } - - for (int i = 0; i < ui->transitions->count(); i++) { - tr = GetTransitionComboItem(ui->transitions, i); - - if (!tr) - continue; - - action = menu->addAction(obs_source_get_name(tr)); - action->setProperty("transition_index", i); - - if (qt) { - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); - } else { - action->setProperty("duration", QVariant::fromValue(duration)); - connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); - } - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - return menu; -} - -void OBSBasic::AddQuickTransitionId(int id) -{ - QuickTransition *qt = GetQuickTransition(id); - if (!qt) - return; - - /* --------------------------------- */ - - QPushButton *button = new MenuButton(); - button->setProperty("id", id); - - qt->button = button; - ResetQuickTransitionText(qt); - - /* --------------------------------- */ - - QMenu *buttonMenu = CreateTransitionMenu(button, qt); - - /* --------------------------------- */ - - button->setMenu(buttonMenu); - connect(button, &QAbstractButton::clicked, this, &OBSBasic::QuickTransitionClicked); - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - int idx = 3; - for (;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget || !widget->property("id").isValid()) - break; - } - - programLayout->insertWidget(idx, button); -} - -void OBSBasic::AddQuickTransition() -{ - int trIdx = sender()->property("transition_index").toInt(); - QSpinBox *duration = sender()->property("duration").value(); - bool fadeToBlack = sender()->property("fadeToBlack").value(); - OBSSource transition = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); - - if (!transition) - return; - - int id = quickTransitionIdCounter++; - - quickTransitions.emplace_back(transition, duration->value(), id, fadeToBlack); - AddQuickTransitionId(id); - - int idx = (int)quickTransitions.size() - 1; - AddQuickTransitionHotkey(&quickTransitions[idx]); -} - -void OBSBasic::ClearQuickTransitions() -{ - for (QuickTransition &qt : quickTransitions) - RemoveQuickTransitionHotkey(&qt); - quickTransitions.clear(); - - if (!programOptions) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget) - continue; - - int id = widget->property("id").toInt(); - if (id != 0) { - delete widget; - idx--; - } - } -} - -void OBSBasic::QuickTransitionClicked() -{ - int id = sender()->property("id").toInt(); - TriggerQuickTransition(id); -} - -void OBSBasic::QuickTransitionChange() -{ - int id = sender()->property("id").toInt(); - int trIdx = sender()->property("transition_index").toInt(); - bool fadeToBlack = sender()->property("fadeToBlack").value(); - QuickTransition *qt = GetQuickTransition(id); - - if (qt) { - OBSSource tr = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); - if (tr) { - qt->source = tr; - qt->fadeToBlack = fadeToBlack; - ResetQuickTransitionText(qt); - } - } -} - -void OBSBasic::QuickTransitionChangeDuration(int value) -{ - int id = sender()->property("id").toInt(); - QuickTransition *qt = GetQuickTransition(id); - - if (qt) { - qt->duration = value; - ResetQuickTransitionText(qt); - } -} - -void OBSBasic::QuickTransitionRemoveClicked() -{ - int id = sender()->property("id").toInt(); - int idx = GetQuickTransitionIdx(id); - if (idx == -1) - return; - - QuickTransition &qt = quickTransitions[idx]; - - if (qt.button) - qt.button->deleteLater(); - - RemoveQuickTransitionHotkey(&qt); - quickTransitions.erase(quickTransitions.begin() + idx); -} - -void OBSBasic::ClearQuickTransitionWidgets() -{ - if (!IsPreviewProgramMode()) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget) - continue; - - int id = widget->property("id").toInt(); - if (id != 0) { - delete widget; - idx--; - } - } -} - -void OBSBasic::RefreshQuickTransitions() -{ - if (!IsPreviewProgramMode()) - return; - - for (QuickTransition &qt : quickTransitions) - AddQuickTransitionId(qt.id); -} - -void OBSBasic::EnableTransitionWidgets(bool enable) -{ - ui->transitions->setEnabled(enable); - - if (!enable) { - ui->transitionProps->setEnabled(false); - } else { - bool configurable = obs_source_configurable(GetCurrentTransition()); - ui->transitionProps->setEnabled(configurable); - } - - if (!IsPreviewProgramMode()) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QPushButton *button = qobject_cast(item->widget()); - if (!button) - continue; - - button->setEnabled(enable); - } - - if (transitionButton) - transitionButton->setEnabled(enable); -} - -void OBSBasic::SetPreviewProgramMode(bool enabled) -{ - if (IsPreviewProgramMode() == enabled) - return; - - os_atomic_set_bool(&previewProgramMode, enabled); - emit PreviewProgramModeChanged(enabled); - - if (IsPreviewProgramMode()) { - if (!previewEnabled) - EnablePreviewDisplay(true); - - CreateProgramDisplay(); - CreateProgramOptions(); - - OBSScene curScene = GetCurrentScene(); - - OBSSceneAutoRelease dup; - if (sceneDuplicationMode) { - dup = obs_scene_duplicate(curScene, obs_source_get_name(obs_scene_get_source(curScene)), - editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY - : OBS_SCENE_DUP_PRIVATE_REFS); - } else { - dup = std::move(OBSScene(curScene)); - } - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *dup_source = obs_scene_get_source(dup); - obs_transition_set(transition, dup_source); - - if (curScene) { - obs_source_t *source = obs_scene_get_source(curScene); - obs_source_inc_showing(source); - lastScene = OBSGetWeakRef(source); - programScene = OBSGetWeakRef(source); - } - - RefreshQuickTransitions(); - - programLabel = new QLabel(QTStr("StudioMode.ProgramSceneLabel"), this); - programLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); - programLabel->setProperty("class", "label-preview-title"); - - programWidget = new QWidget(); - programLayout = new QVBoxLayout(); - - programLayout->setContentsMargins(0, 0, 0, 0); - programLayout->setSpacing(0); - - programLayout->addWidget(programLabel); - programLayout->addWidget(program); - - programWidget->setLayout(programLayout); - - ui->previewLayout->addWidget(programOptions); - ui->previewLayout->addWidget(programWidget); - ui->previewLayout->setAlignment(programOptions, Qt::AlignCenter); - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED); - - blog(LOG_INFO, "Switched to Preview/Program mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } else { - OBSSource actualProgramScene = OBSGetStrongRef(programScene); - if (!actualProgramScene) - actualProgramScene = GetCurrentSceneSource(); - else - SetCurrentScene(actualProgramScene, true); - TransitionToScene(actualProgramScene, true); - - delete programOptions; - delete program; - delete programLabel; - delete programWidget; - - if (lastScene) { - OBSSource actualLastScene = OBSGetStrongRef(lastScene); - if (actualLastScene) - obs_source_dec_showing(actualLastScene); - lastScene = nullptr; - } - - programScene = nullptr; - swapScene = nullptr; - prevFTBSource = nullptr; - - for (QuickTransition &qt : quickTransitions) - qt.button = nullptr; - - if (!previewEnabled) - EnablePreviewDisplay(false); - - ui->transitions->setEnabled(true); - tBarActive = false; - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED); - - blog(LOG_INFO, "Switched to regular Preview mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } - - ResetUI(); - UpdateTitleBar(); -} - -void OBSBasic::RenderProgram(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderProgram"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->programCX = int(window->programScale * float(ovi.base_width)); - window->programCY = int(window->programScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->programX, window->programY, window->programCX, window->programCY); - - obs_render_main_texture_src_color_only(); - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - - /* resize program panel to fix to the top section of the window */ - targetSize = GetPixelSize(program); - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, programX, programY, programScale); - - programX += float(PREVIEW_EDGE_SIZE); - programY += float(PREVIEW_EDGE_SIZE); -} - -obs_data_array_t *OBSBasic::SaveTransitions() -{ - obs_data_array_t *transitions = obs_data_array_create(); - - for (int i = 0; i < ui->transitions->count(); i++) { - OBSSource tr = ui->transitions->itemData(i).value(); - if (!tr || !obs_source_configurable(tr)) - continue; - - OBSDataAutoRelease sourceData = obs_data_create(); - OBSDataAutoRelease settings = obs_source_get_settings(tr); - - obs_data_set_string(sourceData, "name", obs_source_get_name(tr)); - obs_data_set_string(sourceData, "id", obs_obj_get_id(tr)); - obs_data_set_obj(sourceData, "settings", settings); - - obs_data_array_push_back(transitions, sourceData); - } - - for (const OBSDataAutoRelease &transition : safeModeTransitions) { - obs_data_array_push_back(transitions, transition); - } - - return transitions; -} - -void OBSBasic::LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data) -{ - size_t count = obs_data_array_count(transitions); - - safeModeTransitions.clear(); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease item = obs_data_array_item(transitions, i); - const char *name = obs_data_get_string(item, "name"); - const char *id = obs_data_get_string(item, "id"); - OBSDataAutoRelease settings = obs_data_get_obj(item, "settings"); - - OBSSourceAutoRelease source = obs_source_create_private(id, name, settings); - if (!obs_obj_invalid(source)) { - InitTransition(source); - - ui->transitions->addItem(QT_UTF8(name), QVariant::fromValue(OBSSource(source))); - ui->transitions->setCurrentIndex(ui->transitions->count() - 1); - if (cb) - cb(private_data, source); - } else if (safe_mode || disable_3p_plugins) { - safeModeTransitions.push_back(std::move(item)); - } - } -} - -OBSSource OBSBasic::GetOverrideTransition(OBSSource source) -{ - if (!source) - return nullptr; - - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - const char *trOverrideName = obs_data_get_string(data, "transition"); - - OBSSource trOverride = nullptr; - - if (trOverrideName && *trOverrideName) - trOverride = FindTransition(trOverrideName); - - return trOverride; -} - -int OBSBasic::GetOverrideTransitionDuration(OBSSource source) -{ - if (!source) - return 300; - - OBSDataAutoRelease data = obs_source_get_private_settings(source); - obs_data_set_default_int(data, "transition_duration", 300); - - return (int)obs_data_get_int(data, "transition_duration"); -} - -void OBSBasic::UpdatePreviewProgramIndicators() -{ - bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") - : false; - - ui->previewLabel->setVisible(labels); - - if (programLabel) - programLabel->setVisible(labels); - - if (!labels) - return; - - QString preview = - QTStr("StudioMode.PreviewSceneName").arg(QT_UTF8(obs_source_get_name(GetCurrentSceneSource()))); - - QString program = QTStr("StudioMode.ProgramSceneName").arg(QT_UTF8(obs_source_get_name(GetProgramSource()))); - - if (ui->previewLabel->text() != preview) - ui->previewLabel->setText(preview); - - if (programLabel && programLabel->text() != program) - programLabel->setText(program); -} diff --git a/frontend/utility/QuickTransition.hpp b/frontend/utility/QuickTransition.hpp index 81bb7b478..a17f107f2 100644 --- a/frontend/utility/QuickTransition.hpp +++ b/frontend/utility/QuickTransition.hpp @@ -17,96 +17,13 @@ #pragma once -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include "window-main.hpp" -#include "window-basic-interaction.hpp" -#include "window-basic-vcam.hpp" -#include "window-basic-properties.hpp" -#include "window-basic-transform.hpp" -#include "window-basic-adv-audio.hpp" -#include "window-basic-filters.hpp" -#include "window-missing-files.hpp" -#include "window-projector.hpp" -#include "window-basic-about.hpp" -#ifdef YOUTUBE_ENABLED -#include "window-dock-youtube-app.hpp" -#endif -#include "auth-base.hpp" -#include "log-viewer.hpp" -#include "undo-stack-obs.hpp" -#include +#include -#include -#include -#include +using namespace std; -#include - -class QMessageBox; -class QListWidgetItem; -class VolControl; -class OBSBasicStats; -class OBSBasicVCamConfig; - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") -#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") -#define AUX_AUDIO_1 Str("AuxAudioDevice1") -#define AUX_AUDIO_2 Str("AuxAudioDevice2") -#define AUX_AUDIO_3 Str("AuxAudioDevice3") -#define AUX_AUDIO_4 Str("AuxAudioDevice4") - -#define SIMPLE_ENCODER_X264 "x264" -#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" -#define SIMPLE_ENCODER_QSV "qsv" -#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" -#define SIMPLE_ENCODER_NVENC "nvenc" -#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" -#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" -#define SIMPLE_ENCODER_AMD "amd" -#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" -#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" -#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" -#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" - -#define PREVIEW_EDGE_SIZE 10 - -struct BasicOutputHandler; - -enum class QtDataRole { - OBSRef = Qt::UserRole, - OBSSignals, -}; - -struct SavedProjectorInfo { - ProjectorType type; - int monitor; - std::string geometry; - std::string name; - bool alwaysOnTop; - bool alwaysOnTopOverridden; -}; - -struct SourceCopyInfo { - OBSWeakSource weak_source; - bool visible; - obs_sceneitem_crop crop; - obs_transform_info transform; - obs_blending_method blend_method; - obs_blending_type blend_mode; -}; +class QPushButton; struct QuickTransition { QPushButton *button = nullptr; @@ -131,1252 +48,3 @@ private: static void SourceRenamed(void *param, calldata_t *data); std::shared_ptr renamedSignal; }; - -struct OBSProfile { - std::string name; - std::string directoryName; - std::filesystem::path path; - std::filesystem::path profileFile; -}; - -struct OBSSceneCollection { - std::string name; - std::string fileName; - std::filesystem::path collectionFile; -}; - -struct OBSPromptResult { - bool success; - std::string promptValue; - bool optionValue; -}; - -struct OBSPromptRequest { - std::string title; - std::string prompt; - std::string promptValue; - bool withOption; - std::string optionPrompt; - bool optionValue; -}; - -using OBSPromptCallback = std::function; - -using OBSProfileCache = std::map; -using OBSSceneCollectionCache = std::map; - -class ColorSelect : public QWidget { - -public: - explicit ColorSelect(QWidget *parent = 0); - -private: - std::unique_ptr ui; -}; - -class OBSBasic : public OBSMainWindow { - Q_OBJECT - Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) - Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) - Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) - Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) - Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) - Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) - Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) - Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) - Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) - Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon - DESIGNABLE true) - - friend class OBSAbout; - friend class OBSBasicPreview; - friend class OBSBasicStatusBar; - friend class OBSBasicSourceSelect; - friend class OBSBasicTransform; - friend class OBSBasicSettings; - friend class Auth; - friend class AutoConfig; - friend class AutoConfigStreamPage; - friend class RecordButton; - friend class ControlsSplitButton; - friend class ExtraBrowsersModel; - friend class ExtraBrowsersDelegate; - friend class DeviceCaptureToolbar; - friend class OBSBasicSourceSelect; - friend class OBSYoutubeActions; - friend class OBSPermissions; - friend struct BasicOutputHandler; - friend struct OBSStudioAPI; - friend class ScreenshotObj; - - enum class MoveDir { Up, Down, Left, Right }; - - enum DropType { - DropType_RawText, - DropType_Text, - DropType_Image, - DropType_Media, - DropType_Html, - DropType_Url, - }; - - enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; - - enum class CenterType { - Scene, - Vertical, - Horizontal, - }; - -private: - obs_frontend_callbacks *api = nullptr; - - std::shared_ptr auth; - - std::vector volumes; - - std::vector signalHandlers; - - QList> oldExtraDocks; - QStringList oldExtraDockNames; - - OBSDataAutoRelease collectionModuleData; - std::vector safeModeTransitions; - - bool loaded = false; - long disableSaving = 1; - bool projectChanged = false; - bool previewEnabled = true; - ContextBarSize contextBarSize = ContextBarSize_Normal; - - std::deque clipboard; - OBSWeakSourceAutoRelease copyFiltersSource; - bool copyVisible = true; - obs_transform_info copiedTransformInfo; - obs_sceneitem_crop copiedCropInfo; - bool hasCopiedTransform = false; - OBSWeakSourceAutoRelease copySourceTransition; - int copySourceTransitionDuration; - - bool closing = false; - bool clearingFailed = false; - - QScopedPointer devicePropertiesThread; - QScopedPointer whatsNewInitThread; - QScopedPointer updateCheckThread; - QScopedPointer introCheckThread; - QScopedPointer logUploadThread; - - QPointer interaction; - QPointer properties; - QPointer transformWindow; - QPointer advAudioWindow; - QPointer filters; - QPointer statsDock; -#ifdef YOUTUBE_ENABLED - QPointer youtubeAppDock; - uint64_t lastYouTubeAppDockCreationTime = 0; -#endif - QPointer about; - QPointer missDialog; - QPointer logView; - - QPointer cpuUsageTimer; - QPointer diskFullTimer; - - QPointer nudge_timer; - bool recent_nudge = false; - - os_cpu_usage_info_t *cpuUsageInfo = nullptr; - - OBSService service; - std::unique_ptr outputHandler; - std::shared_future setupStreamingGuard; - bool streamingStopping = false; - bool recordingStopping = false; - bool replayBufferStopping = false; - - gs_vertbuffer_t *box = nullptr; - gs_vertbuffer_t *boxLeft = nullptr; - gs_vertbuffer_t *boxTop = nullptr; - gs_vertbuffer_t *boxRight = nullptr; - gs_vertbuffer_t *boxBottom = nullptr; - gs_vertbuffer_t *circle = nullptr; - - gs_vertbuffer_t *actionSafeMargin = nullptr; - gs_vertbuffer_t *graphicsSafeMargin = nullptr; - gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; - - int previewX = 0, previewY = 0; - int previewCX = 0, previewCY = 0; - float previewScale = 0.0f; - - ConfigFile activeConfiguration; - - std::vector savedProjectorsArray; - std::vector projectors; - - QPointer stats; - QPointer remux; - QPointer extraBrowsers; - QPointer importer; - - QPointer transitionButton; - - bool vcamEnabled = false; - VCamConfig vcamConfig; - - QScopedPointer trayIcon; - QPointer sysTrayStream; - QPointer sysTrayRecord; - QPointer sysTrayReplayBuffer; - QPointer sysTrayVirtualCam; - QPointer showHide; - QPointer exit; - QPointer trayMenu; - QPointer previewProjector; - QPointer studioProgramProjector; - QPointer previewProjectorSource; - QPointer previewProjectorMain; - QPointer sceneProjectorMenu; - QPointer sourceProjector; - QPointer scaleFilteringMenu; - QPointer blendingMethodMenu; - QPointer blendingModeMenu; - QPointer colorMenu; - QPointer colorWidgetAction; - QPointer colorSelect; - QPointer deinterlaceMenu; - QPointer perSceneTransitionMenu; - QPointer shortcutFilter; - QPointer renameScene; - QPointer renameSource; - - QPointer programWidget; - QPointer programLayout; - QPointer programLabel; - - QScopedPointer patronJsonThread; - std::string patronJson; - - std::atomic currentScene = nullptr; - std::optional> lastOutputResolution; - std::optional> migrationBaseResolution; - bool usingAbsoluteCoordinates = false; - - void DisableRelativeCoordinates(bool disable); - - void OnEvent(enum obs_frontend_event event); - - void UpdateMultiviewProjectorMenu(); - - void DrawBackdrop(float cx, float cy); - - void SetupEncoders(); - - void CreateFirstRunSources(); - void CreateDefaultScene(bool firstStart); - - void UpdateVolumeControlsDecayRate(); - void UpdateVolumeControlsPeakMeterType(); - void ClearVolumeControls(); - - void UploadLog(const char *subdir, const char *file, const bool crash); - - void Save(const char *file); - void LoadData(obs_data_t *data, const char *file, bool remigrate = false); - void Load(const char *file, bool remigrate = false); - - void InitHotkeys(); - void CreateHotkeys(); - void ClearHotkeys(); - - bool InitService(); - - bool InitBasicConfigDefaults(); - void InitBasicConfigDefaults2(); - bool InitBasicConfig(); - - void InitOBSCallbacks(); - - void InitPrimitives(); - - void OnFirstLoad(); - - OBSSceneItem GetSceneItem(QListWidgetItem *item); - OBSSceneItem GetCurrentSceneItem(); - - bool QueryRemoveSource(obs_source_t *source); - - void TimedCheckForUpdates(); - void CheckForUpdates(bool manualUpdate); - - void GetFPSCommon(uint32_t &num, uint32_t &den) const; - void GetFPSInteger(uint32_t &num, uint32_t &den) const; - void GetFPSFraction(uint32_t &num, uint32_t &den) const; - void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; - void GetConfigFPS(uint32_t &num, uint32_t &den) const; - - void UpdatePreviewScalingMenu(); - - void LoadSceneListOrder(obs_data_array_t *array); - obs_data_array_t *SaveSceneListOrder(); - void ChangeSceneIndex(bool relative, int idx, int invalidIdx); - - void TempFileOutput(const char *path, int vBitrate, int aBitrate); - void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); - - void CloseDialogs(); - void ClearSceneData(); - void ClearProjectors(); - - void Nudge(int dist, MoveDir dir); - - OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); - - void GetAudioSourceFilters(); - void GetAudioSourceProperties(); - void VolControlContextMenu(); - void ToggleVolControlLayout(); - void ToggleMixerLayout(bool vertical); - - void LogScenes(); - void SaveProjectNow(); - - int GetTopSelectedSourceItem(); - - QModelIndexList GetAllSelectedSourceItems(); - - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, - togglePreviewHotkeys, contextBarHotkeys; - obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; - - void InitDefaultTransitions(); - void InitTransition(obs_source_t *transition); - obs_source_t *FindTransition(const char *name); - OBSSource GetCurrentTransition(); - obs_data_array_t *SaveTransitions(); - void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); - - obs_source_t *fadeTransition; - obs_source_t *cutTransition; - - void CreateProgramDisplay(); - void CreateProgramOptions(); - void AddQuickTransitionId(int id); - void AddQuickTransition(); - void AddQuickTransitionHotkey(QuickTransition *qt); - void RemoveQuickTransitionHotkey(QuickTransition *qt); - void LoadQuickTransitions(obs_data_array_t *array); - obs_data_array_t *SaveQuickTransitions(); - void ClearQuickTransitionWidgets(); - void RefreshQuickTransitions(); - void DisableQuickTransitionWidgets(); - void EnableTransitionWidgets(bool enable); - void CreateDefaultQuickTransitions(); - - void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); - QMenu *CreatePerSceneTransitionMenu(); - QMenu *CreateVisibilityTransitionMenu(bool visible); - - QuickTransition *GetQuickTransition(int id); - int GetQuickTransitionIdx(int id); - QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); - void ClearQuickTransitions(); - void QuickTransitionClicked(); - void QuickTransitionChange(); - void QuickTransitionChangeDuration(int value); - void QuickTransitionRemoveClicked(); - - void SetPreviewProgramMode(bool enabled); - void ResizeProgram(uint32_t cx, uint32_t cy); - void SetCurrentScene(obs_scene_t *scene, bool force = false); - static void RenderProgram(void *data, uint32_t cx, uint32_t cy); - - std::vector quickTransitions; - QPointer programOptions; - QPointer program; - OBSWeakSource lastScene; - OBSWeakSource swapScene; - OBSWeakSource programScene; - OBSWeakSource lastProgramScene; - bool editPropertiesMode = false; - bool sceneDuplicationMode = true; - bool swapScenesMode = true; - volatile bool previewProgramMode = false; - obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; - obs_hotkey_id transitionHotkey = 0; - obs_hotkey_id statsHotkey = 0; - obs_hotkey_id screenshotHotkey = 0; - obs_hotkey_id sourceScreenshotHotkey = 0; - int quickTransitionIdCounter = 1; - bool overridingTransition = false; - - int programX = 0, programY = 0; - int programCX = 0, programCY = 0; - float programScale = 0.0f; - - int disableOutputsRef = 0; - - inline void OnActivate(bool force = false); - inline void OnDeactivate(); - - void AddDropSource(const char *file, DropType image); - void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - - bool sysTrayMinimizeToTray(); - - void EnumDialogs(); - - QList visDialogs; - QList modalDialogs; - QList visMsgBoxes; - - QList visDlgPositions; - - QByteArray startingDockLayout; - - obs_data_array_t *SaveProjectors(); - void LoadSavedProjectors(obs_data_array_t *savedProjectors); - - void MacBranchesFetched(const QString &branch, bool manualUpdate); - void ReceivedIntroJson(const QString &text); - void ShowWhatsNew(const QString &url); - - void UpdatePreviewProgramIndicators(); - - QStringList extraDockNames; - QList> extraDocks; - - QStringList extraCustomDockNames; - QList> extraCustomDocks; - -#ifdef BROWSER_AVAILABLE - QPointer extraBrowserMenuDocksSeparator; - - QList> extraBrowserDocks; - QStringList extraBrowserDockNames; - QStringList extraBrowserDockTargets; - - void ClearExtraBrowserDocks(); - void LoadExtraBrowserDocks(); - void SaveExtraBrowserDocks(); - void ManageExtraBrowserDocks(); - void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); -#endif - - QIcon imageIcon; - QIcon colorIcon; - QIcon slideshowIcon; - QIcon audioInputIcon; - QIcon audioOutputIcon; - QIcon desktopCapIcon; - QIcon windowCapIcon; - QIcon gameCapIcon; - QIcon cameraIcon; - QIcon textIcon; - QIcon mediaIcon; - QIcon browserIcon; - QIcon groupIcon; - QIcon sceneIcon; - QIcon defaultIcon; - QIcon audioProcessOutputIcon; - - QIcon GetImageIcon() const; - QIcon GetColorIcon() const; - QIcon GetSlideshowIcon() const; - QIcon GetAudioInputIcon() const; - QIcon GetAudioOutputIcon() const; - QIcon GetDesktopCapIcon() const; - QIcon GetWindowCapIcon() const; - QIcon GetGameCapIcon() const; - QIcon GetCameraIcon() const; - QIcon GetTextIcon() const; - QIcon GetMediaIcon() const; - QIcon GetBrowserIcon() const; - QIcon GetDefaultIcon() const; - QIcon GetAudioProcessOutputIcon() const; - - QSlider *tBar; - bool tBarActive = false; - - OBSSource GetOverrideTransition(OBSSource source); - int GetOverrideTransitionDuration(OBSSource source); - - void UpdateProjectorHideCursor(); - void UpdateProjectorAlwaysOnTop(bool top); - void ResetProjectors(); - - QPointer screenshotData; - - void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); - - void UpdatePreviewSafeAreas(); - bool drawSafeAreas = false; - - void CenterSelectedSceneItems(const CenterType ¢erType); - void ShowMissingFilesDialog(obs_missing_files_t *files); - - QColor selectionColor; - QColor cropColor; - QColor hoverColor; - - QColor GetCropColor() const; - QColor GetHoverColor() const; - - void UpdatePreviewSpacingHelpers(); - bool drawSpacingHelpers = true; - - float GetDevicePixelRatio(); - void SourceToolBarActionsSetEnabled(); - - std::string lastScreenshot; - std::string lastReplay; - - void UpdatePreviewOverflowSettings(); - void UpdatePreviewScrollbars(); - - bool streamingStarting = false; - - bool recordingStarted = false; - bool isRecordingPausable = false; - bool recordingPaused = false; - - bool restartingVCam = false; - -public slots: - void DeferSaveBegin(); - void DeferSaveEnd(); - - void DisplayStreamStartError(); - - void SetupBroadcast(); - - void StartStreaming(); - void StopStreaming(); - void ForceStopStreaming(); - - void StreamDelayStarting(int sec); - void StreamDelayStopping(int sec); - - void StreamingStart(); - void StreamStopping(); - void StreamingStop(int errorcode, QString last_error); - - void StartRecording(); - void StopRecording(); - - void RecordingStart(); - void RecordStopping(); - void RecordingStop(int code, QString last_error); - void RecordingFileChanged(QString lastRecordingPath); - - void ShowReplayBufferPauseWarning(); - void StartReplayBuffer(); - void StopReplayBuffer(); - - void ReplayBufferStart(); - void ReplayBufferSave(); - void ReplayBufferSaved(); - void ReplayBufferStopping(); - void ReplayBufferStop(int code); - - void StartVirtualCam(); - void StopVirtualCam(); - - void OnVirtualCamStart(); - void OnVirtualCamStop(int code); - - void SaveProjectDeferred(); - void SaveProject(); - - void SetTransition(OBSSource transition); - void OverrideTransition(OBSSource transition); - void TransitionToScene(OBSScene scene, bool force = false); - void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, - bool black = false, bool manual = false); - void SetCurrentScene(OBSSource scene, bool force = false); - - void UpdatePatronJson(const QString &text, const QString &error); - - void ShowContextBar(); - void HideContextBar(); - void PauseRecording(); - void UnpauseRecording(); - - void UpdateEditMenu(); - -private slots: - - void on_actionMainUndo_triggered(); - void on_actionMainRedo_triggered(); - - void AddSceneItem(OBSSceneItem item); - void AddScene(OBSSource source); - void RemoveScene(OBSSource source); - void RenameSources(OBSSource source, QString newName, QString prevName); - - void ActivateAudioSource(OBSSource source); - void DeactivateAudioSource(OBSSource source); - - void DuplicateSelectedScene(); - void RemoveSelectedScene(); - - void ToggleAlwaysOnTop(); - - void ReorderSources(OBSScene scene); - void RefreshSources(OBSScene scene); - - void ProcessHotkey(obs_hotkey_id id, bool pressed); - - void AddTransition(const char *id); - void RenameTransition(OBSSource transition); - void TransitionClicked(); - void TransitionStopped(); - void TransitionFullyStopped(); - void TriggerQuickTransition(int id); - - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - - void IconActivated(QSystemTrayIcon::ActivationReason reason); - void SetShowing(bool showing); - - void ToggleShowHide(); - - void HideAudioControl(); - void UnhideAllAudioControls(); - void ToggleHideMixer(); - - void MixerRenameSource(); - - void on_vMixerScrollArea_customContextMenuRequested(); - void on_hMixerScrollArea_customContextMenuRequested(); - - void on_actionCopySource_triggered(); - void on_actionPasteRef_triggered(); - void on_actionPasteDup_triggered(); - - void on_actionCopyFilters_triggered(); - void on_actionPasteFilters_triggered(); - void AudioMixerCopyFilters(); - void AudioMixerPasteFilters(); - void SourcePasteFilters(OBSSource source, OBSSource dstSource); - - void on_previewXScrollBar_valueChanged(int value); - void on_previewYScrollBar_valueChanged(int value); - - void PreviewScalingModeChanged(int value); - - void ColorChange(); - - SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); - - void on_actionShowAbout_triggered(); - - void EnablePreview(); - void DisablePreview(); - - void EnablePreviewProgram(); - void DisablePreviewProgram(); - - void SceneCopyFilters(); - void ScenePasteFilters(); - - void CheckDiskSpaceRemaining(); - void OpenSavedProjector(SavedProjectorInfo *info); - - void ResetStatsHotkey(); - - void SetImageIcon(const QIcon &icon); - void SetColorIcon(const QIcon &icon); - void SetSlideshowIcon(const QIcon &icon); - void SetAudioInputIcon(const QIcon &icon); - void SetAudioOutputIcon(const QIcon &icon); - void SetDesktopCapIcon(const QIcon &icon); - void SetWindowCapIcon(const QIcon &icon); - void SetGameCapIcon(const QIcon &icon); - void SetCameraIcon(const QIcon &icon); - void SetTextIcon(const QIcon &icon); - void SetMediaIcon(const QIcon &icon); - void SetBrowserIcon(const QIcon &icon); - void SetGroupIcon(const QIcon &icon); - void SetSceneIcon(const QIcon &icon); - void SetDefaultIcon(const QIcon &icon); - void SetAudioProcessOutputIcon(const QIcon &icon); - - void TBarChanged(int value); - void TBarReleased(); - - void LockVolumeControl(bool lock); - - void UpdateVirtualCamConfig(const VCamConfig &config); - void RestartVirtualCam(const VCamConfig &config); - void RestartingVirtualCam(); - -private: - /* OBS Callbacks */ - static void SceneReordered(void *data, calldata_t *params); - static void SceneRefreshed(void *data, calldata_t *params); - static void SceneItemAdded(void *data, calldata_t *params); - static void SourceCreated(void *data, calldata_t *params); - static void SourceRemoved(void *data, calldata_t *params); - static void SourceActivated(void *data, calldata_t *params); - static void SourceDeactivated(void *data, calldata_t *params); - static void SourceAudioActivated(void *data, calldata_t *params); - static void SourceAudioDeactivated(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void RenderMain(void *data, uint32_t cx, uint32_t cy); - - void ResizePreview(uint32_t cx, uint32_t cy); - - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - void copyActionsDynamicProperties(); - - static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - - void AutoRemux(QString input, bool no_show = false); - - void UpdateIsRecordingPausable(); - - bool IsFFmpegOutputToURL() const; - bool OutputPathValid(); - void OutputPathInvalidMessage(); - - bool LowDiskSpace(); - void DiskSpaceMessage(); - - OBSSource prevFTBSource = nullptr; - - float dpi = 1.0; - -public: - OBSSource GetProgramSource(); - OBSScene GetCurrentScene(); - - void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); - - inline OBSSource GetCurrentSceneSource() - { - OBSScene curScene = GetCurrentScene(); - return OBSSource(obs_scene_get_source(curScene)); - } - - obs_service_t *GetService(); - void SetService(obs_service_t *service); - - int GetTransitionDuration(); - int GetTbarPosition(); - - inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } - - inline bool VCamEnabled() const { return vcamEnabled; } - - bool Active() const; - - void ResetUI(); - int ResetVideo(); - bool ResetAudio(); - - void ResetOutputs(); - - void RefreshVolumeColors(); - - void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - - void NewProject(); - void LoadProject(); - - inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) - { - x = previewX; - y = previewY; - cx = previewCX; - cy = previewCY; - } - - inline bool SavingDisabled() const { return disableSaving; } - - inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } - - void SaveService(); - bool LoadService(); - - inline Auth *GetAuth() { return auth.get(); } - - inline void EnableOutputs(bool enable) - { - if (enable) { - if (--disableOutputsRef < 0) - disableOutputsRef = 0; - } else { - disableOutputsRef++; - } - } - - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item); - void CreateSourcePopupMenu(int idx, bool preview); - - void UpdateTitleBar(); - - void SystemTrayInit(); - void SystemTray(bool firstStarted); - - void OpenSavedProjectors(); - - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CreateEditTransformWindow(obs_sceneitem_t *item); - - QAction *AddDockWidget(QDockWidget *dock); - void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); - void RemoveDockWidget(const QString &name); - bool IsDockObjectNameUsed(const QString &name); - void AddCustomDockWidget(QDockWidget *dock); - - static OBSBasic *Get(); - - const char *GetCurrentOutputPath(); - - void DeleteProjector(OBSProjector *projector); - - static QList GetProjectorMenuMonitorsFormatted(); - template - static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) - { - auto projectors = GetProjectorMenuMonitorsFormatted(); - for (int i = 0; i < projectors.size(); i++) { - QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); - action->setProperty("monitor", i); - } - } - - QIcon GetSourceIcon(const char *id) const; - QIcon GetGroupIcon() const; - QIcon GetSceneIcon() const; - - OBSWeakSource copyFilter; - - void ShowStatusBarMessage(const QString &message); - - static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); - void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); - - static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) - { - obs_scene_t *scene = obs_scene_from_source(scene_source); - return BackupScene(scene, sources); - } - - void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array); - - void SetDisplayAffinity(QWindow *window); - - QColor GetSelectionColor() const; - inline bool Closing() { return closing; } - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; - virtual void changeEvent(QEvent *event) override; - -private slots: - void on_actionFullscreenInterface_triggered(); - - void on_actionShow_Recordings_triggered(); - void on_actionRemux_triggered(); - void on_action_Settings_triggered(); - void on_actionShowMacPermissions_triggered(); - void on_actionShowMissingFiles_triggered(); - void on_actionAdvAudioProperties_triggered(); - void on_actionMixerToolbarAdvAudio_triggered(); - void on_actionMixerToolbarMenu_triggered(); - void on_actionShowLogs_triggered(); - void on_actionUploadCurrentLog_triggered(); - void on_actionUploadLastLog_triggered(); - void on_actionViewCurrentLog_triggered(); - void on_actionCheckForUpdates_triggered(); - void on_actionRepair_triggered(); - void on_actionShowWhatsNew_triggered(); - void on_actionRestartSafe_triggered(); - - void on_actionShowCrashLogs_triggered(); - void on_actionUploadLastCrashLog_triggered(); - - void on_actionEditTransform_triggered(); - void on_actionCopyTransform_triggered(); - void on_actionPasteTransform_triggered(); - void on_actionRotate90CW_triggered(); - void on_actionRotate90CCW_triggered(); - void on_actionRotate180_triggered(); - void on_actionFlipHorizontal_triggered(); - void on_actionFlipVertical_triggered(); - void on_actionFitToScreen_triggered(); - void on_actionStretchToScreen_triggered(); - void on_actionCenterToScreen_triggered(); - void on_actionVerticalCenter_triggered(); - void on_actionHorizontalCenter_triggered(); - void on_actionSceneFilters_triggered(); - - void on_OBSBasic_customContextMenuRequested(const QPoint &pos); - - void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); - void on_scenes_customContextMenuRequested(const QPoint &pos); - void GridActionClicked(); - void on_actionSceneListMode_triggered(); - void on_actionSceneGridMode_triggered(); - void on_actionAddScene_triggered(); - void on_actionRemoveScene_triggered(); - void on_actionSceneUp_triggered(); - void on_actionSceneDown_triggered(); - void on_sources_customContextMenuRequested(const QPoint &pos); - void on_scenes_itemDoubleClicked(QListWidgetItem *item); - void on_actionAddSource_triggered(); - void on_actionRemoveSource_triggered(); - void on_actionInteract_triggered(); - void on_actionSourceProperties_triggered(); - void on_actionSourceUp_triggered(); - void on_actionSourceDown_triggered(); - - void on_actionMoveUp_triggered(); - void on_actionMoveDown_triggered(); - void on_actionMoveToTop_triggered(); - void on_actionMoveToBottom_triggered(); - - void on_actionLockPreview_triggered(); - - void on_scalingMenu_aboutToShow(); - void on_actionScaleWindow_triggered(); - void on_actionScaleCanvas_triggered(); - void on_actionScaleOutput_triggered(); - - void Screenshot(OBSSource source_ = nullptr); - void ScreenshotSelectedSource(); - void ScreenshotProgram(); - void ScreenshotScene(); - - void on_actionHelpPortal_triggered(); - void on_actionWebsite_triggered(); - void on_actionDiscord_triggered(); - void on_actionReleaseNotes_triggered(); - - void on_preview_customContextMenuRequested(); - void ProgramViewContextMenuRequested(); - void on_previewDisabledWidget_customContextMenuRequested(); - - void on_actionShowSettingsFolder_triggered(); - void on_actionShowProfileFolder_triggered(); - - void on_actionAlwaysOnTop_triggered(); - - void on_toggleListboxToolbars_toggled(bool visible); - void on_toggleContextBar_toggled(bool visible); - void on_toggleStatusBar_toggled(bool visible); - void on_toggleSourceIcons_toggled(bool visible); - - void on_transitions_currentIndexChanged(int index); - void on_transitionAdd_clicked(); - void on_transitionRemove_clicked(); - void on_transitionProps_clicked(); - void on_transitionDuration_valueChanged(); - - void ShowTransitionProperties(); - void HideTransitionProperties(); - - // Source Context Buttons - void on_sourcePropertiesButton_clicked(); - void on_sourceFiltersButton_clicked(); - void on_sourceInteractButton_clicked(); - - void on_autoConfigure_triggered(); - void on_stats_triggered(); - - void on_resetUI_triggered(); - void on_resetDocks_triggered(bool force = false); - void on_lockDocks_toggled(bool lock); - void on_multiviewProjectorWindowed_triggered(); - void on_sideDocks_toggled(bool side); - - void logUploadFinished(const QString &text, const QString &error); - void crashUploadFinished(const QString &text, const QString &error); - void openLogDialog(const QString &text, const bool crash); - - void updateCheckFinished(); - - void MoveSceneToTop(); - void MoveSceneToBottom(); - - void EditSceneName(); - void EditSceneItemName(); - - void SceneNameEdited(QWidget *editor); - - void OpenSceneFilters(); - void OpenFilters(OBSSource source = nullptr); - void OpenProperties(OBSSource source = nullptr); - void OpenInteraction(OBSSource source = nullptr); - void OpenEditTransform(OBSSceneItem item = nullptr); - - void EnablePreviewDisplay(bool enable); - void TogglePreview(); - - void OpenStudioProgramProjector(); - void OpenPreviewProjector(); - void OpenSourceProjector(); - void OpenMultiviewProjector(); - void OpenSceneProjector(); - - void OpenStudioProgramWindow(); - void OpenPreviewWindow(); - void OpenSourceWindow(); - void OpenSceneWindow(); - - void StackedMixerAreaContextMenuRequested(); - - void ResizeOutputSizeOfSource(); - - void RepairOldExtraDockName(); - void RepairCustomExtraDockName(); - - /* Stream action (start/stop) slot */ - void StreamActionTriggered(); - - /* Record action (start/stop) slot */ - void RecordActionTriggered(); - - /* Record pause (pause/unpause) slot */ - void RecordPauseToggled(); - - /* Replay Buffer action (start/stop) slot */ - void ReplayBufferActionTriggered(); - - /* Virtual Cam action (start/stop) slots */ - void VirtualCamActionTriggered(); - - void OpenVirtualCamConfig(); - - /* Studio Mode toggle slot */ - void TogglePreviewProgramMode(); - -public slots: - void on_actionResetTransform_triggered(); - - bool StreamingActive(); - bool RecordingActive(); - bool ReplayBufferActive(); - bool VirtualCamActive(); - - void ClearContextBar(); - void UpdateContextBar(bool force = false); - void UpdateContextBarDeferred(bool force = false); - void UpdateContextBarVisibility(); - -signals: - /* Streaming signals */ - void StreamingPreparing(); - void StreamingStarting(bool broadcastAutoStart); - void StreamingStarted(bool withDelay = false); - void StreamingStopping(); - void StreamingStopped(bool withDelay = false); - - /* Broadcast Flow signals */ - void BroadcastFlowEnabled(bool enabled); - void BroadcastStreamReady(bool ready); - void BroadcastStreamActive(); - void BroadcastStreamStarted(bool autoStop); - - /* Recording signals */ - void RecordingStarted(bool pausable = false); - void RecordingPaused(); - void RecordingUnpaused(); - void RecordingStopping(); - void RecordingStopped(); - - /* Replay Buffer signals */ - void ReplayBufEnabled(bool enabled); - void ReplayBufStarted(); - void ReplayBufStopping(); - void ReplayBufStopped(); - - /* Virtual Camera signals */ - void VirtualCamEnabled(); - void VirtualCamStarted(); - void VirtualCamStopped(); - - /* Studio Mode signal */ - void PreviewProgramModeChanged(bool enabled); - void CanvasResized(uint32_t width, uint32_t height); - void OutputResized(uint32_t width, uint32_t height); - - /* Preview signals */ - void PreviewXScrollBarMoved(int value); - void PreviewYScrollBarMoved(int value); - -private: - std::unique_ptr ui; - - QPointer controlsDock; - -public: - /* `undo_s` needs to be declared after `ui` to prevent an uninitialized - * warning for `ui` while initializing `undo_s`. */ - undo_stack undo_s; - - explicit OBSBasic(QWidget *parent = 0); - virtual ~OBSBasic(); - - virtual void OBSInit() override; - - virtual config_t *Config() const override; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const override; - - static void InitBrowserPanelSafeBlock(); -#ifdef YOUTUBE_ENABLED - void NewYouTubeAppDock(); - void DeleteYouTubeAppDock(); - YouTubeAppDock *GetYouTubeAppDock(); -#endif - // MARK: - Generic UI Helper Functions - OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - - // MARK: - OBS Profile Management -private: - OBSProfileCache profiles{}; - - void SetupNewProfile(const std::string &profileName, bool useWizard = false); - void SetupDuplicateProfile(const std::string &profileName); - void SetupRenameProfile(const std::string &profileName); - - const OBSProfile &CreateProfile(const std::string &profileName); - void RemoveProfile(OBSProfile profile); - - void ChangeProfile(); - - void RefreshProfileCache(); - - void RefreshProfiles(bool refreshCache = false); - - void ActivateProfile(const OBSProfile &profile, bool reset = false); - void UpdateProfileEncoders(); - std::vector GetRestartRequirements(const ConfigFile &config) const; - void ResetProfileData(); - void CheckForSimpleModeX264Fallback(); - -public: - inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; - - const OBSProfile &GetCurrentProfile() const; - - std::optional GetProfileByName(const std::string &profileName) const; - std::optional GetProfileByDirectoryName(const std::string &directoryName) const; - -private slots: - void on_actionNewProfile_triggered(); - void on_actionDupProfile_triggered(); - void on_actionRenameProfile_triggered(); - void on_actionRemoveProfile_triggered(bool skipConfirmation = false); - void on_actionImportProfile_triggered(); - void on_actionExportProfile_triggered(); - -public slots: - bool CreateNewProfile(const QString &name); - bool CreateDuplicateProfile(const QString &name); - void DeleteProfile(const QString &profileName); - - // MARK: - OBS Scene Collection Management -private: - OBSSceneCollectionCache collections{}; - - void SetupNewSceneCollection(const std::string &collectionName); - void SetupDuplicateSceneCollection(const std::string &collectionName); - void SetupRenameSceneCollection(const std::string &collectionName); - - const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); - void RemoveSceneCollection(OBSSceneCollection collection); - - bool CreateDuplicateSceneCollection(const QString &name); - void DeleteSceneCollection(const QString &name); - void ChangeSceneCollection(); - - void RefreshSceneCollectionCache(); - - void RefreshSceneCollections(bool refreshCache = false); - void ActivateSceneCollection(const OBSSceneCollection &collection); - -public: - inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; - - const OBSSceneCollection &GetCurrentSceneCollection() const; - - std::optional GetSceneCollectionByName(const std::string &collectionName) const; - std::optional GetSceneCollectionByFileName(const std::string &fileName) const; - -private slots: - void on_actionNewSceneCollection_triggered(); - void on_actionDupSceneCollection_triggered(); - void on_actionRenameSceneCollection_triggered(); - void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); - void on_actionImportSceneCollection_triggered(); - void on_actionExportSceneCollection_triggered(); - void on_actionRemigrateSceneCollection_triggered(); - -public slots: - bool CreateNewSceneCollection(const QString &name); -}; - -extern bool cef_js_avail; - -class SceneRenameDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SceneRenameDelegate(QObject *parent); - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - -protected: - virtual bool eventFilter(QObject *editor, QEvent *event) override; -}; diff --git a/frontend/utility/SceneRenameDelegate.cpp b/frontend/utility/SceneRenameDelegate.cpp index 47cac8dcc..e505c5205 100644 --- a/frontend/utility/SceneRenameDelegate.cpp +++ b/frontend/utility/SceneRenameDelegate.cpp @@ -16,9714 +16,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "SceneRenameDelegate.hpp" -#include -#include -#include -#include -#include +#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} +#include "moc_SceneRenameDelegate.cpp" SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} @@ -9754,450 +53,3 @@ bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) return QStyledItemDelegate::eventFilter(editor, event); } - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/utility/SceneRenameDelegate.hpp b/frontend/utility/SceneRenameDelegate.hpp index 81bb7b478..d5212f32e 100644 --- a/frontend/utility/SceneRenameDelegate.hpp +++ b/frontend/utility/SceneRenameDelegate.hpp @@ -17,1358 +17,7 @@ #pragma once -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include "window-main.hpp" -#include "window-basic-interaction.hpp" -#include "window-basic-vcam.hpp" -#include "window-basic-properties.hpp" -#include "window-basic-transform.hpp" -#include "window-basic-adv-audio.hpp" -#include "window-basic-filters.hpp" -#include "window-missing-files.hpp" -#include "window-projector.hpp" -#include "window-basic-about.hpp" -#ifdef YOUTUBE_ENABLED -#include "window-dock-youtube-app.hpp" -#endif -#include "auth-base.hpp" -#include "log-viewer.hpp" -#include "undo-stack-obs.hpp" - -#include - -#include -#include -#include - -#include - -class QMessageBox; -class QListWidgetItem; -class VolControl; -class OBSBasicStats; -class OBSBasicVCamConfig; - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") -#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") -#define AUX_AUDIO_1 Str("AuxAudioDevice1") -#define AUX_AUDIO_2 Str("AuxAudioDevice2") -#define AUX_AUDIO_3 Str("AuxAudioDevice3") -#define AUX_AUDIO_4 Str("AuxAudioDevice4") - -#define SIMPLE_ENCODER_X264 "x264" -#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" -#define SIMPLE_ENCODER_QSV "qsv" -#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" -#define SIMPLE_ENCODER_NVENC "nvenc" -#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" -#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" -#define SIMPLE_ENCODER_AMD "amd" -#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" -#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" -#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" -#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" - -#define PREVIEW_EDGE_SIZE 10 - -struct BasicOutputHandler; - -enum class QtDataRole { - OBSRef = Qt::UserRole, - OBSSignals, -}; - -struct SavedProjectorInfo { - ProjectorType type; - int monitor; - std::string geometry; - std::string name; - bool alwaysOnTop; - bool alwaysOnTopOverridden; -}; - -struct SourceCopyInfo { - OBSWeakSource weak_source; - bool visible; - obs_sceneitem_crop crop; - obs_transform_info transform; - obs_blending_method blend_method; - obs_blending_type blend_mode; -}; - -struct QuickTransition { - QPushButton *button = nullptr; - OBSSource source; - obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; - int duration = 0; - int id = 0; - bool fadeToBlack = false; - - inline QuickTransition() {} - inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) - : source(source_), - duration(duration_), - id(id_), - fadeToBlack(fadeToBlack_), - renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", - SourceRenamed, this)) - { - } - -private: - static void SourceRenamed(void *param, calldata_t *data); - std::shared_ptr renamedSignal; -}; - -struct OBSProfile { - std::string name; - std::string directoryName; - std::filesystem::path path; - std::filesystem::path profileFile; -}; - -struct OBSSceneCollection { - std::string name; - std::string fileName; - std::filesystem::path collectionFile; -}; - -struct OBSPromptResult { - bool success; - std::string promptValue; - bool optionValue; -}; - -struct OBSPromptRequest { - std::string title; - std::string prompt; - std::string promptValue; - bool withOption; - std::string optionPrompt; - bool optionValue; -}; - -using OBSPromptCallback = std::function; - -using OBSProfileCache = std::map; -using OBSSceneCollectionCache = std::map; - -class ColorSelect : public QWidget { - -public: - explicit ColorSelect(QWidget *parent = 0); - -private: - std::unique_ptr ui; -}; - -class OBSBasic : public OBSMainWindow { - Q_OBJECT - Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) - Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) - Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) - Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) - Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) - Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) - Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) - Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) - Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) - Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon - DESIGNABLE true) - - friend class OBSAbout; - friend class OBSBasicPreview; - friend class OBSBasicStatusBar; - friend class OBSBasicSourceSelect; - friend class OBSBasicTransform; - friend class OBSBasicSettings; - friend class Auth; - friend class AutoConfig; - friend class AutoConfigStreamPage; - friend class RecordButton; - friend class ControlsSplitButton; - friend class ExtraBrowsersModel; - friend class ExtraBrowsersDelegate; - friend class DeviceCaptureToolbar; - friend class OBSBasicSourceSelect; - friend class OBSYoutubeActions; - friend class OBSPermissions; - friend struct BasicOutputHandler; - friend struct OBSStudioAPI; - friend class ScreenshotObj; - - enum class MoveDir { Up, Down, Left, Right }; - - enum DropType { - DropType_RawText, - DropType_Text, - DropType_Image, - DropType_Media, - DropType_Html, - DropType_Url, - }; - - enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; - - enum class CenterType { - Scene, - Vertical, - Horizontal, - }; - -private: - obs_frontend_callbacks *api = nullptr; - - std::shared_ptr auth; - - std::vector volumes; - - std::vector signalHandlers; - - QList> oldExtraDocks; - QStringList oldExtraDockNames; - - OBSDataAutoRelease collectionModuleData; - std::vector safeModeTransitions; - - bool loaded = false; - long disableSaving = 1; - bool projectChanged = false; - bool previewEnabled = true; - ContextBarSize contextBarSize = ContextBarSize_Normal; - - std::deque clipboard; - OBSWeakSourceAutoRelease copyFiltersSource; - bool copyVisible = true; - obs_transform_info copiedTransformInfo; - obs_sceneitem_crop copiedCropInfo; - bool hasCopiedTransform = false; - OBSWeakSourceAutoRelease copySourceTransition; - int copySourceTransitionDuration; - - bool closing = false; - bool clearingFailed = false; - - QScopedPointer devicePropertiesThread; - QScopedPointer whatsNewInitThread; - QScopedPointer updateCheckThread; - QScopedPointer introCheckThread; - QScopedPointer logUploadThread; - - QPointer interaction; - QPointer properties; - QPointer transformWindow; - QPointer advAudioWindow; - QPointer filters; - QPointer statsDock; -#ifdef YOUTUBE_ENABLED - QPointer youtubeAppDock; - uint64_t lastYouTubeAppDockCreationTime = 0; -#endif - QPointer about; - QPointer missDialog; - QPointer logView; - - QPointer cpuUsageTimer; - QPointer diskFullTimer; - - QPointer nudge_timer; - bool recent_nudge = false; - - os_cpu_usage_info_t *cpuUsageInfo = nullptr; - - OBSService service; - std::unique_ptr outputHandler; - std::shared_future setupStreamingGuard; - bool streamingStopping = false; - bool recordingStopping = false; - bool replayBufferStopping = false; - - gs_vertbuffer_t *box = nullptr; - gs_vertbuffer_t *boxLeft = nullptr; - gs_vertbuffer_t *boxTop = nullptr; - gs_vertbuffer_t *boxRight = nullptr; - gs_vertbuffer_t *boxBottom = nullptr; - gs_vertbuffer_t *circle = nullptr; - - gs_vertbuffer_t *actionSafeMargin = nullptr; - gs_vertbuffer_t *graphicsSafeMargin = nullptr; - gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; - - int previewX = 0, previewY = 0; - int previewCX = 0, previewCY = 0; - float previewScale = 0.0f; - - ConfigFile activeConfiguration; - - std::vector savedProjectorsArray; - std::vector projectors; - - QPointer stats; - QPointer remux; - QPointer extraBrowsers; - QPointer importer; - - QPointer transitionButton; - - bool vcamEnabled = false; - VCamConfig vcamConfig; - - QScopedPointer trayIcon; - QPointer sysTrayStream; - QPointer sysTrayRecord; - QPointer sysTrayReplayBuffer; - QPointer sysTrayVirtualCam; - QPointer showHide; - QPointer exit; - QPointer trayMenu; - QPointer previewProjector; - QPointer studioProgramProjector; - QPointer previewProjectorSource; - QPointer previewProjectorMain; - QPointer sceneProjectorMenu; - QPointer sourceProjector; - QPointer scaleFilteringMenu; - QPointer blendingMethodMenu; - QPointer blendingModeMenu; - QPointer colorMenu; - QPointer colorWidgetAction; - QPointer colorSelect; - QPointer deinterlaceMenu; - QPointer perSceneTransitionMenu; - QPointer shortcutFilter; - QPointer renameScene; - QPointer renameSource; - - QPointer programWidget; - QPointer programLayout; - QPointer programLabel; - - QScopedPointer patronJsonThread; - std::string patronJson; - - std::atomic currentScene = nullptr; - std::optional> lastOutputResolution; - std::optional> migrationBaseResolution; - bool usingAbsoluteCoordinates = false; - - void DisableRelativeCoordinates(bool disable); - - void OnEvent(enum obs_frontend_event event); - - void UpdateMultiviewProjectorMenu(); - - void DrawBackdrop(float cx, float cy); - - void SetupEncoders(); - - void CreateFirstRunSources(); - void CreateDefaultScene(bool firstStart); - - void UpdateVolumeControlsDecayRate(); - void UpdateVolumeControlsPeakMeterType(); - void ClearVolumeControls(); - - void UploadLog(const char *subdir, const char *file, const bool crash); - - void Save(const char *file); - void LoadData(obs_data_t *data, const char *file, bool remigrate = false); - void Load(const char *file, bool remigrate = false); - - void InitHotkeys(); - void CreateHotkeys(); - void ClearHotkeys(); - - bool InitService(); - - bool InitBasicConfigDefaults(); - void InitBasicConfigDefaults2(); - bool InitBasicConfig(); - - void InitOBSCallbacks(); - - void InitPrimitives(); - - void OnFirstLoad(); - - OBSSceneItem GetSceneItem(QListWidgetItem *item); - OBSSceneItem GetCurrentSceneItem(); - - bool QueryRemoveSource(obs_source_t *source); - - void TimedCheckForUpdates(); - void CheckForUpdates(bool manualUpdate); - - void GetFPSCommon(uint32_t &num, uint32_t &den) const; - void GetFPSInteger(uint32_t &num, uint32_t &den) const; - void GetFPSFraction(uint32_t &num, uint32_t &den) const; - void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; - void GetConfigFPS(uint32_t &num, uint32_t &den) const; - - void UpdatePreviewScalingMenu(); - - void LoadSceneListOrder(obs_data_array_t *array); - obs_data_array_t *SaveSceneListOrder(); - void ChangeSceneIndex(bool relative, int idx, int invalidIdx); - - void TempFileOutput(const char *path, int vBitrate, int aBitrate); - void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); - - void CloseDialogs(); - void ClearSceneData(); - void ClearProjectors(); - - void Nudge(int dist, MoveDir dir); - - OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); - - void GetAudioSourceFilters(); - void GetAudioSourceProperties(); - void VolControlContextMenu(); - void ToggleVolControlLayout(); - void ToggleMixerLayout(bool vertical); - - void LogScenes(); - void SaveProjectNow(); - - int GetTopSelectedSourceItem(); - - QModelIndexList GetAllSelectedSourceItems(); - - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, - togglePreviewHotkeys, contextBarHotkeys; - obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; - - void InitDefaultTransitions(); - void InitTransition(obs_source_t *transition); - obs_source_t *FindTransition(const char *name); - OBSSource GetCurrentTransition(); - obs_data_array_t *SaveTransitions(); - void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); - - obs_source_t *fadeTransition; - obs_source_t *cutTransition; - - void CreateProgramDisplay(); - void CreateProgramOptions(); - void AddQuickTransitionId(int id); - void AddQuickTransition(); - void AddQuickTransitionHotkey(QuickTransition *qt); - void RemoveQuickTransitionHotkey(QuickTransition *qt); - void LoadQuickTransitions(obs_data_array_t *array); - obs_data_array_t *SaveQuickTransitions(); - void ClearQuickTransitionWidgets(); - void RefreshQuickTransitions(); - void DisableQuickTransitionWidgets(); - void EnableTransitionWidgets(bool enable); - void CreateDefaultQuickTransitions(); - - void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); - QMenu *CreatePerSceneTransitionMenu(); - QMenu *CreateVisibilityTransitionMenu(bool visible); - - QuickTransition *GetQuickTransition(int id); - int GetQuickTransitionIdx(int id); - QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); - void ClearQuickTransitions(); - void QuickTransitionClicked(); - void QuickTransitionChange(); - void QuickTransitionChangeDuration(int value); - void QuickTransitionRemoveClicked(); - - void SetPreviewProgramMode(bool enabled); - void ResizeProgram(uint32_t cx, uint32_t cy); - void SetCurrentScene(obs_scene_t *scene, bool force = false); - static void RenderProgram(void *data, uint32_t cx, uint32_t cy); - - std::vector quickTransitions; - QPointer programOptions; - QPointer program; - OBSWeakSource lastScene; - OBSWeakSource swapScene; - OBSWeakSource programScene; - OBSWeakSource lastProgramScene; - bool editPropertiesMode = false; - bool sceneDuplicationMode = true; - bool swapScenesMode = true; - volatile bool previewProgramMode = false; - obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; - obs_hotkey_id transitionHotkey = 0; - obs_hotkey_id statsHotkey = 0; - obs_hotkey_id screenshotHotkey = 0; - obs_hotkey_id sourceScreenshotHotkey = 0; - int quickTransitionIdCounter = 1; - bool overridingTransition = false; - - int programX = 0, programY = 0; - int programCX = 0, programCY = 0; - float programScale = 0.0f; - - int disableOutputsRef = 0; - - inline void OnActivate(bool force = false); - inline void OnDeactivate(); - - void AddDropSource(const char *file, DropType image); - void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - - bool sysTrayMinimizeToTray(); - - void EnumDialogs(); - - QList visDialogs; - QList modalDialogs; - QList visMsgBoxes; - - QList visDlgPositions; - - QByteArray startingDockLayout; - - obs_data_array_t *SaveProjectors(); - void LoadSavedProjectors(obs_data_array_t *savedProjectors); - - void MacBranchesFetched(const QString &branch, bool manualUpdate); - void ReceivedIntroJson(const QString &text); - void ShowWhatsNew(const QString &url); - - void UpdatePreviewProgramIndicators(); - - QStringList extraDockNames; - QList> extraDocks; - - QStringList extraCustomDockNames; - QList> extraCustomDocks; - -#ifdef BROWSER_AVAILABLE - QPointer extraBrowserMenuDocksSeparator; - - QList> extraBrowserDocks; - QStringList extraBrowserDockNames; - QStringList extraBrowserDockTargets; - - void ClearExtraBrowserDocks(); - void LoadExtraBrowserDocks(); - void SaveExtraBrowserDocks(); - void ManageExtraBrowserDocks(); - void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); -#endif - - QIcon imageIcon; - QIcon colorIcon; - QIcon slideshowIcon; - QIcon audioInputIcon; - QIcon audioOutputIcon; - QIcon desktopCapIcon; - QIcon windowCapIcon; - QIcon gameCapIcon; - QIcon cameraIcon; - QIcon textIcon; - QIcon mediaIcon; - QIcon browserIcon; - QIcon groupIcon; - QIcon sceneIcon; - QIcon defaultIcon; - QIcon audioProcessOutputIcon; - - QIcon GetImageIcon() const; - QIcon GetColorIcon() const; - QIcon GetSlideshowIcon() const; - QIcon GetAudioInputIcon() const; - QIcon GetAudioOutputIcon() const; - QIcon GetDesktopCapIcon() const; - QIcon GetWindowCapIcon() const; - QIcon GetGameCapIcon() const; - QIcon GetCameraIcon() const; - QIcon GetTextIcon() const; - QIcon GetMediaIcon() const; - QIcon GetBrowserIcon() const; - QIcon GetDefaultIcon() const; - QIcon GetAudioProcessOutputIcon() const; - - QSlider *tBar; - bool tBarActive = false; - - OBSSource GetOverrideTransition(OBSSource source); - int GetOverrideTransitionDuration(OBSSource source); - - void UpdateProjectorHideCursor(); - void UpdateProjectorAlwaysOnTop(bool top); - void ResetProjectors(); - - QPointer screenshotData; - - void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); - - void UpdatePreviewSafeAreas(); - bool drawSafeAreas = false; - - void CenterSelectedSceneItems(const CenterType ¢erType); - void ShowMissingFilesDialog(obs_missing_files_t *files); - - QColor selectionColor; - QColor cropColor; - QColor hoverColor; - - QColor GetCropColor() const; - QColor GetHoverColor() const; - - void UpdatePreviewSpacingHelpers(); - bool drawSpacingHelpers = true; - - float GetDevicePixelRatio(); - void SourceToolBarActionsSetEnabled(); - - std::string lastScreenshot; - std::string lastReplay; - - void UpdatePreviewOverflowSettings(); - void UpdatePreviewScrollbars(); - - bool streamingStarting = false; - - bool recordingStarted = false; - bool isRecordingPausable = false; - bool recordingPaused = false; - - bool restartingVCam = false; - -public slots: - void DeferSaveBegin(); - void DeferSaveEnd(); - - void DisplayStreamStartError(); - - void SetupBroadcast(); - - void StartStreaming(); - void StopStreaming(); - void ForceStopStreaming(); - - void StreamDelayStarting(int sec); - void StreamDelayStopping(int sec); - - void StreamingStart(); - void StreamStopping(); - void StreamingStop(int errorcode, QString last_error); - - void StartRecording(); - void StopRecording(); - - void RecordingStart(); - void RecordStopping(); - void RecordingStop(int code, QString last_error); - void RecordingFileChanged(QString lastRecordingPath); - - void ShowReplayBufferPauseWarning(); - void StartReplayBuffer(); - void StopReplayBuffer(); - - void ReplayBufferStart(); - void ReplayBufferSave(); - void ReplayBufferSaved(); - void ReplayBufferStopping(); - void ReplayBufferStop(int code); - - void StartVirtualCam(); - void StopVirtualCam(); - - void OnVirtualCamStart(); - void OnVirtualCamStop(int code); - - void SaveProjectDeferred(); - void SaveProject(); - - void SetTransition(OBSSource transition); - void OverrideTransition(OBSSource transition); - void TransitionToScene(OBSScene scene, bool force = false); - void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, - bool black = false, bool manual = false); - void SetCurrentScene(OBSSource scene, bool force = false); - - void UpdatePatronJson(const QString &text, const QString &error); - - void ShowContextBar(); - void HideContextBar(); - void PauseRecording(); - void UnpauseRecording(); - - void UpdateEditMenu(); - -private slots: - - void on_actionMainUndo_triggered(); - void on_actionMainRedo_triggered(); - - void AddSceneItem(OBSSceneItem item); - void AddScene(OBSSource source); - void RemoveScene(OBSSource source); - void RenameSources(OBSSource source, QString newName, QString prevName); - - void ActivateAudioSource(OBSSource source); - void DeactivateAudioSource(OBSSource source); - - void DuplicateSelectedScene(); - void RemoveSelectedScene(); - - void ToggleAlwaysOnTop(); - - void ReorderSources(OBSScene scene); - void RefreshSources(OBSScene scene); - - void ProcessHotkey(obs_hotkey_id id, bool pressed); - - void AddTransition(const char *id); - void RenameTransition(OBSSource transition); - void TransitionClicked(); - void TransitionStopped(); - void TransitionFullyStopped(); - void TriggerQuickTransition(int id); - - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - - void IconActivated(QSystemTrayIcon::ActivationReason reason); - void SetShowing(bool showing); - - void ToggleShowHide(); - - void HideAudioControl(); - void UnhideAllAudioControls(); - void ToggleHideMixer(); - - void MixerRenameSource(); - - void on_vMixerScrollArea_customContextMenuRequested(); - void on_hMixerScrollArea_customContextMenuRequested(); - - void on_actionCopySource_triggered(); - void on_actionPasteRef_triggered(); - void on_actionPasteDup_triggered(); - - void on_actionCopyFilters_triggered(); - void on_actionPasteFilters_triggered(); - void AudioMixerCopyFilters(); - void AudioMixerPasteFilters(); - void SourcePasteFilters(OBSSource source, OBSSource dstSource); - - void on_previewXScrollBar_valueChanged(int value); - void on_previewYScrollBar_valueChanged(int value); - - void PreviewScalingModeChanged(int value); - - void ColorChange(); - - SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); - - void on_actionShowAbout_triggered(); - - void EnablePreview(); - void DisablePreview(); - - void EnablePreviewProgram(); - void DisablePreviewProgram(); - - void SceneCopyFilters(); - void ScenePasteFilters(); - - void CheckDiskSpaceRemaining(); - void OpenSavedProjector(SavedProjectorInfo *info); - - void ResetStatsHotkey(); - - void SetImageIcon(const QIcon &icon); - void SetColorIcon(const QIcon &icon); - void SetSlideshowIcon(const QIcon &icon); - void SetAudioInputIcon(const QIcon &icon); - void SetAudioOutputIcon(const QIcon &icon); - void SetDesktopCapIcon(const QIcon &icon); - void SetWindowCapIcon(const QIcon &icon); - void SetGameCapIcon(const QIcon &icon); - void SetCameraIcon(const QIcon &icon); - void SetTextIcon(const QIcon &icon); - void SetMediaIcon(const QIcon &icon); - void SetBrowserIcon(const QIcon &icon); - void SetGroupIcon(const QIcon &icon); - void SetSceneIcon(const QIcon &icon); - void SetDefaultIcon(const QIcon &icon); - void SetAudioProcessOutputIcon(const QIcon &icon); - - void TBarChanged(int value); - void TBarReleased(); - - void LockVolumeControl(bool lock); - - void UpdateVirtualCamConfig(const VCamConfig &config); - void RestartVirtualCam(const VCamConfig &config); - void RestartingVirtualCam(); - -private: - /* OBS Callbacks */ - static void SceneReordered(void *data, calldata_t *params); - static void SceneRefreshed(void *data, calldata_t *params); - static void SceneItemAdded(void *data, calldata_t *params); - static void SourceCreated(void *data, calldata_t *params); - static void SourceRemoved(void *data, calldata_t *params); - static void SourceActivated(void *data, calldata_t *params); - static void SourceDeactivated(void *data, calldata_t *params); - static void SourceAudioActivated(void *data, calldata_t *params); - static void SourceAudioDeactivated(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void RenderMain(void *data, uint32_t cx, uint32_t cy); - - void ResizePreview(uint32_t cx, uint32_t cy); - - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - void copyActionsDynamicProperties(); - - static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - - void AutoRemux(QString input, bool no_show = false); - - void UpdateIsRecordingPausable(); - - bool IsFFmpegOutputToURL() const; - bool OutputPathValid(); - void OutputPathInvalidMessage(); - - bool LowDiskSpace(); - void DiskSpaceMessage(); - - OBSSource prevFTBSource = nullptr; - - float dpi = 1.0; - -public: - OBSSource GetProgramSource(); - OBSScene GetCurrentScene(); - - void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); - - inline OBSSource GetCurrentSceneSource() - { - OBSScene curScene = GetCurrentScene(); - return OBSSource(obs_scene_get_source(curScene)); - } - - obs_service_t *GetService(); - void SetService(obs_service_t *service); - - int GetTransitionDuration(); - int GetTbarPosition(); - - inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } - - inline bool VCamEnabled() const { return vcamEnabled; } - - bool Active() const; - - void ResetUI(); - int ResetVideo(); - bool ResetAudio(); - - void ResetOutputs(); - - void RefreshVolumeColors(); - - void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - - void NewProject(); - void LoadProject(); - - inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) - { - x = previewX; - y = previewY; - cx = previewCX; - cy = previewCY; - } - - inline bool SavingDisabled() const { return disableSaving; } - - inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } - - void SaveService(); - bool LoadService(); - - inline Auth *GetAuth() { return auth.get(); } - - inline void EnableOutputs(bool enable) - { - if (enable) { - if (--disableOutputsRef < 0) - disableOutputsRef = 0; - } else { - disableOutputsRef++; - } - } - - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item); - void CreateSourcePopupMenu(int idx, bool preview); - - void UpdateTitleBar(); - - void SystemTrayInit(); - void SystemTray(bool firstStarted); - - void OpenSavedProjectors(); - - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CreateEditTransformWindow(obs_sceneitem_t *item); - - QAction *AddDockWidget(QDockWidget *dock); - void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); - void RemoveDockWidget(const QString &name); - bool IsDockObjectNameUsed(const QString &name); - void AddCustomDockWidget(QDockWidget *dock); - - static OBSBasic *Get(); - - const char *GetCurrentOutputPath(); - - void DeleteProjector(OBSProjector *projector); - - static QList GetProjectorMenuMonitorsFormatted(); - template - static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) - { - auto projectors = GetProjectorMenuMonitorsFormatted(); - for (int i = 0; i < projectors.size(); i++) { - QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); - action->setProperty("monitor", i); - } - } - - QIcon GetSourceIcon(const char *id) const; - QIcon GetGroupIcon() const; - QIcon GetSceneIcon() const; - - OBSWeakSource copyFilter; - - void ShowStatusBarMessage(const QString &message); - - static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); - void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); - - static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) - { - obs_scene_t *scene = obs_scene_from_source(scene_source); - return BackupScene(scene, sources); - } - - void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array); - - void SetDisplayAffinity(QWindow *window); - - QColor GetSelectionColor() const; - inline bool Closing() { return closing; } - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; - virtual void changeEvent(QEvent *event) override; - -private slots: - void on_actionFullscreenInterface_triggered(); - - void on_actionShow_Recordings_triggered(); - void on_actionRemux_triggered(); - void on_action_Settings_triggered(); - void on_actionShowMacPermissions_triggered(); - void on_actionShowMissingFiles_triggered(); - void on_actionAdvAudioProperties_triggered(); - void on_actionMixerToolbarAdvAudio_triggered(); - void on_actionMixerToolbarMenu_triggered(); - void on_actionShowLogs_triggered(); - void on_actionUploadCurrentLog_triggered(); - void on_actionUploadLastLog_triggered(); - void on_actionViewCurrentLog_triggered(); - void on_actionCheckForUpdates_triggered(); - void on_actionRepair_triggered(); - void on_actionShowWhatsNew_triggered(); - void on_actionRestartSafe_triggered(); - - void on_actionShowCrashLogs_triggered(); - void on_actionUploadLastCrashLog_triggered(); - - void on_actionEditTransform_triggered(); - void on_actionCopyTransform_triggered(); - void on_actionPasteTransform_triggered(); - void on_actionRotate90CW_triggered(); - void on_actionRotate90CCW_triggered(); - void on_actionRotate180_triggered(); - void on_actionFlipHorizontal_triggered(); - void on_actionFlipVertical_triggered(); - void on_actionFitToScreen_triggered(); - void on_actionStretchToScreen_triggered(); - void on_actionCenterToScreen_triggered(); - void on_actionVerticalCenter_triggered(); - void on_actionHorizontalCenter_triggered(); - void on_actionSceneFilters_triggered(); - - void on_OBSBasic_customContextMenuRequested(const QPoint &pos); - - void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); - void on_scenes_customContextMenuRequested(const QPoint &pos); - void GridActionClicked(); - void on_actionSceneListMode_triggered(); - void on_actionSceneGridMode_triggered(); - void on_actionAddScene_triggered(); - void on_actionRemoveScene_triggered(); - void on_actionSceneUp_triggered(); - void on_actionSceneDown_triggered(); - void on_sources_customContextMenuRequested(const QPoint &pos); - void on_scenes_itemDoubleClicked(QListWidgetItem *item); - void on_actionAddSource_triggered(); - void on_actionRemoveSource_triggered(); - void on_actionInteract_triggered(); - void on_actionSourceProperties_triggered(); - void on_actionSourceUp_triggered(); - void on_actionSourceDown_triggered(); - - void on_actionMoveUp_triggered(); - void on_actionMoveDown_triggered(); - void on_actionMoveToTop_triggered(); - void on_actionMoveToBottom_triggered(); - - void on_actionLockPreview_triggered(); - - void on_scalingMenu_aboutToShow(); - void on_actionScaleWindow_triggered(); - void on_actionScaleCanvas_triggered(); - void on_actionScaleOutput_triggered(); - - void Screenshot(OBSSource source_ = nullptr); - void ScreenshotSelectedSource(); - void ScreenshotProgram(); - void ScreenshotScene(); - - void on_actionHelpPortal_triggered(); - void on_actionWebsite_triggered(); - void on_actionDiscord_triggered(); - void on_actionReleaseNotes_triggered(); - - void on_preview_customContextMenuRequested(); - void ProgramViewContextMenuRequested(); - void on_previewDisabledWidget_customContextMenuRequested(); - - void on_actionShowSettingsFolder_triggered(); - void on_actionShowProfileFolder_triggered(); - - void on_actionAlwaysOnTop_triggered(); - - void on_toggleListboxToolbars_toggled(bool visible); - void on_toggleContextBar_toggled(bool visible); - void on_toggleStatusBar_toggled(bool visible); - void on_toggleSourceIcons_toggled(bool visible); - - void on_transitions_currentIndexChanged(int index); - void on_transitionAdd_clicked(); - void on_transitionRemove_clicked(); - void on_transitionProps_clicked(); - void on_transitionDuration_valueChanged(); - - void ShowTransitionProperties(); - void HideTransitionProperties(); - - // Source Context Buttons - void on_sourcePropertiesButton_clicked(); - void on_sourceFiltersButton_clicked(); - void on_sourceInteractButton_clicked(); - - void on_autoConfigure_triggered(); - void on_stats_triggered(); - - void on_resetUI_triggered(); - void on_resetDocks_triggered(bool force = false); - void on_lockDocks_toggled(bool lock); - void on_multiviewProjectorWindowed_triggered(); - void on_sideDocks_toggled(bool side); - - void logUploadFinished(const QString &text, const QString &error); - void crashUploadFinished(const QString &text, const QString &error); - void openLogDialog(const QString &text, const bool crash); - - void updateCheckFinished(); - - void MoveSceneToTop(); - void MoveSceneToBottom(); - - void EditSceneName(); - void EditSceneItemName(); - - void SceneNameEdited(QWidget *editor); - - void OpenSceneFilters(); - void OpenFilters(OBSSource source = nullptr); - void OpenProperties(OBSSource source = nullptr); - void OpenInteraction(OBSSource source = nullptr); - void OpenEditTransform(OBSSceneItem item = nullptr); - - void EnablePreviewDisplay(bool enable); - void TogglePreview(); - - void OpenStudioProgramProjector(); - void OpenPreviewProjector(); - void OpenSourceProjector(); - void OpenMultiviewProjector(); - void OpenSceneProjector(); - - void OpenStudioProgramWindow(); - void OpenPreviewWindow(); - void OpenSourceWindow(); - void OpenSceneWindow(); - - void StackedMixerAreaContextMenuRequested(); - - void ResizeOutputSizeOfSource(); - - void RepairOldExtraDockName(); - void RepairCustomExtraDockName(); - - /* Stream action (start/stop) slot */ - void StreamActionTriggered(); - - /* Record action (start/stop) slot */ - void RecordActionTriggered(); - - /* Record pause (pause/unpause) slot */ - void RecordPauseToggled(); - - /* Replay Buffer action (start/stop) slot */ - void ReplayBufferActionTriggered(); - - /* Virtual Cam action (start/stop) slots */ - void VirtualCamActionTriggered(); - - void OpenVirtualCamConfig(); - - /* Studio Mode toggle slot */ - void TogglePreviewProgramMode(); - -public slots: - void on_actionResetTransform_triggered(); - - bool StreamingActive(); - bool RecordingActive(); - bool ReplayBufferActive(); - bool VirtualCamActive(); - - void ClearContextBar(); - void UpdateContextBar(bool force = false); - void UpdateContextBarDeferred(bool force = false); - void UpdateContextBarVisibility(); - -signals: - /* Streaming signals */ - void StreamingPreparing(); - void StreamingStarting(bool broadcastAutoStart); - void StreamingStarted(bool withDelay = false); - void StreamingStopping(); - void StreamingStopped(bool withDelay = false); - - /* Broadcast Flow signals */ - void BroadcastFlowEnabled(bool enabled); - void BroadcastStreamReady(bool ready); - void BroadcastStreamActive(); - void BroadcastStreamStarted(bool autoStop); - - /* Recording signals */ - void RecordingStarted(bool pausable = false); - void RecordingPaused(); - void RecordingUnpaused(); - void RecordingStopping(); - void RecordingStopped(); - - /* Replay Buffer signals */ - void ReplayBufEnabled(bool enabled); - void ReplayBufStarted(); - void ReplayBufStopping(); - void ReplayBufStopped(); - - /* Virtual Camera signals */ - void VirtualCamEnabled(); - void VirtualCamStarted(); - void VirtualCamStopped(); - - /* Studio Mode signal */ - void PreviewProgramModeChanged(bool enabled); - void CanvasResized(uint32_t width, uint32_t height); - void OutputResized(uint32_t width, uint32_t height); - - /* Preview signals */ - void PreviewXScrollBarMoved(int value); - void PreviewYScrollBarMoved(int value); - -private: - std::unique_ptr ui; - - QPointer controlsDock; - -public: - /* `undo_s` needs to be declared after `ui` to prevent an uninitialized - * warning for `ui` while initializing `undo_s`. */ - undo_stack undo_s; - - explicit OBSBasic(QWidget *parent = 0); - virtual ~OBSBasic(); - - virtual void OBSInit() override; - - virtual config_t *Config() const override; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const override; - - static void InitBrowserPanelSafeBlock(); -#ifdef YOUTUBE_ENABLED - void NewYouTubeAppDock(); - void DeleteYouTubeAppDock(); - YouTubeAppDock *GetYouTubeAppDock(); -#endif - // MARK: - Generic UI Helper Functions - OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - - // MARK: - OBS Profile Management -private: - OBSProfileCache profiles{}; - - void SetupNewProfile(const std::string &profileName, bool useWizard = false); - void SetupDuplicateProfile(const std::string &profileName); - void SetupRenameProfile(const std::string &profileName); - - const OBSProfile &CreateProfile(const std::string &profileName); - void RemoveProfile(OBSProfile profile); - - void ChangeProfile(); - - void RefreshProfileCache(); - - void RefreshProfiles(bool refreshCache = false); - - void ActivateProfile(const OBSProfile &profile, bool reset = false); - void UpdateProfileEncoders(); - std::vector GetRestartRequirements(const ConfigFile &config) const; - void ResetProfileData(); - void CheckForSimpleModeX264Fallback(); - -public: - inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; - - const OBSProfile &GetCurrentProfile() const; - - std::optional GetProfileByName(const std::string &profileName) const; - std::optional GetProfileByDirectoryName(const std::string &directoryName) const; - -private slots: - void on_actionNewProfile_triggered(); - void on_actionDupProfile_triggered(); - void on_actionRenameProfile_triggered(); - void on_actionRemoveProfile_triggered(bool skipConfirmation = false); - void on_actionImportProfile_triggered(); - void on_actionExportProfile_triggered(); - -public slots: - bool CreateNewProfile(const QString &name); - bool CreateDuplicateProfile(const QString &name); - void DeleteProfile(const QString &profileName); - - // MARK: - OBS Scene Collection Management -private: - OBSSceneCollectionCache collections{}; - - void SetupNewSceneCollection(const std::string &collectionName); - void SetupDuplicateSceneCollection(const std::string &collectionName); - void SetupRenameSceneCollection(const std::string &collectionName); - - const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); - void RemoveSceneCollection(OBSSceneCollection collection); - - bool CreateDuplicateSceneCollection(const QString &name); - void DeleteSceneCollection(const QString &name); - void ChangeSceneCollection(); - - void RefreshSceneCollectionCache(); - - void RefreshSceneCollections(bool refreshCache = false); - void ActivateSceneCollection(const OBSSceneCollection &collection); - -public: - inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; - - const OBSSceneCollection &GetCurrentSceneCollection() const; - - std::optional GetSceneCollectionByName(const std::string &collectionName) const; - std::optional GetSceneCollectionByFileName(const std::string &fileName) const; - -private slots: - void on_actionNewSceneCollection_triggered(); - void on_actionDupSceneCollection_triggered(); - void on_actionRenameSceneCollection_triggered(); - void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); - void on_actionImportSceneCollection_triggered(); - void on_actionExportSceneCollection_triggered(); - void on_actionRemigrateSceneCollection_triggered(); - -public slots: - bool CreateNewSceneCollection(const QString &name); -}; - -extern bool cef_js_avail; class SceneRenameDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/utility/ScreenshotObj.cpp b/frontend/utility/ScreenshotObj.cpp index a0b3622d1..5b7bc86ca 100644 --- a/frontend/utility/ScreenshotObj.cpp +++ b/frontend/utility/ScreenshotObj.cpp @@ -15,8 +15,9 @@ along with this program. If not, see . ******************************************************************************/ -#include "window-basic-main.hpp" -#include "screenshot-obj.hpp" +#include "ScreenshotObj.hpp" + +#include #include @@ -27,9 +28,9 @@ #pragma comment(lib, "windowscodecs.lib") #endif -static void ScreenshotTick(void *param, float); +#include "moc_ScreenshotObj.cpp" -/* ========================================================================= */ +static void ScreenshotTick(void *param, float); ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source)) { @@ -278,8 +279,6 @@ void ScreenshotObj::MuxAndFinish() deleteLater(); } -/* ========================================================================= */ - #define STAGE_SCREENSHOT 0 #define STAGE_DOWNLOAD 1 #define STAGE_COPY_AND_SAVE 2 @@ -313,35 +312,3 @@ static void ScreenshotTick(void *param, float) data->stage++; } - -void OBSBasic::Screenshot(OBSSource source) -{ - if (!!screenshotData) { - blog(LOG_WARNING, "Cannot take new screenshot, " - "screenshot currently in progress"); - return; - } - - screenshotData = new ScreenshotObj(source); -} - -void OBSBasic::ScreenshotSelectedSource() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (item) { - Screenshot(obs_sceneitem_get_source(item)); - } else { - blog(LOG_INFO, "Could not take a source screenshot: " - "no source selected"); - } -} - -void OBSBasic::ScreenshotProgram() -{ - Screenshot(GetProgramSource()); -} - -void OBSBasic::ScreenshotScene() -{ - Screenshot(GetCurrentSceneSource()); -} diff --git a/frontend/utility/SimpleOutput.cpp b/frontend/utility/SimpleOutput.cpp index 30d203174..4e783dd7a 100644 --- a/frontend/utility/SimpleOutput.cpp +++ b/frontend/utility/SimpleOutput.cpp @@ -1,195 +1,13 @@ -#include -#include -#include -#include -#include +#include "SimpleOutput.hpp" + +#include +#include +#include + #include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" using namespace std; -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -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) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) { const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); @@ -226,291 +44,7 @@ static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *na return false; } -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; +extern bool EncoderAvailable(const char *encoder); void SimpleOutput::LoadRecordingPreset_Lossless() { @@ -1025,21 +559,6 @@ inline void SimpleOutput::SetupOutputs() } } -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) { if (!Active()) @@ -1105,24 +624,6 @@ std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, Se }); } -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) { bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); @@ -1401,1141 +902,3 @@ bool SimpleOutput::ReplayBufferActive() const { return obs_output_active(replayBuffer); } - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/frontend/utility/SimpleOutput.hpp b/frontend/utility/SimpleOutput.hpp index 30d203174..46d6fc381 100644 --- a/frontend/utility/SimpleOutput.hpp +++ b/frontend/utility/SimpleOutput.hpp @@ -1,456 +1,6 @@ -#include -#include -#include -#include -#include -#include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" +#pragma once -using namespace std; - -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -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) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ +#include "BasicOutputHandler.hpp" struct SimpleOutput : BasicOutputHandler { OBSEncoder audioStreaming; @@ -460,8 +10,8 @@ struct SimpleOutput : BasicOutputHandler { OBSEncoder videoRecording; OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - string videoEncoder; - string videoQuality; + std::string videoEncoder; + std::string videoQuality; bool usingRecordingPreset = false; bool recordingConfigured = false; bool ffmpegOutput = false; @@ -511,2031 +61,3 @@ struct SimpleOutput : BasicOutputHandler { virtual bool RecordingActive() const override; virtual bool ReplayBufferActive() const override; }; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp index 30d203174..f0535f4d2 100644 --- a/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp +++ b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp @@ -1,175 +1,6 @@ -#include -#include -#include -#include -#include -#include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" +#pragma once -using namespace std; - -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -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) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ +#include struct StartMultitrackVideoStreamingGuard { StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; @@ -187,2355 +18,3 @@ private: std::promise guard; std::shared_future future; }; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.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), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "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"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - 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); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/frontend/utility/SurfaceEventFilter.hpp b/frontend/utility/SurfaceEventFilter.hpp index f7c37ae9a..de6f12600 100644 --- a/frontend/utility/SurfaceEventFilter.hpp +++ b/frontend/utility/SurfaceEventFilter.hpp @@ -1,25 +1,9 @@ -#include "moc_qt-display.cpp" -#include "display-helpers.hpp" -#include -#include -#include -#include +#pragma once -#include -#include +#include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) -#include -#endif - -#ifdef ENABLE_WAYLAND -#include -#endif +#include +#include class SurfaceEventFilter : public QObject { OBSQTDisplay *display; @@ -52,192 +36,3 @@ protected: return result; } }; - -static inline long long color_to_int(const QColor &color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -static inline QColor rgba_to_color(uint32_t rgba) -{ - return QColor::fromRgb(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF, (rgba >> 24) & 0xFF); -} - -static bool QTToGSWindow(QWindow *window, gs_window &gswindow) -{ - bool success = true; - -#ifdef _WIN32 - gswindow.hwnd = (HWND)window->winId(); -#elif __APPLE__ - gswindow.view = (id)window->winId(); -#else - switch (obs_get_nix_platform()) { - case OBS_NIX_PLATFORM_X11_EGL: - gswindow.id = window->winId(); - gswindow.display = obs_get_nix_platform_display(); - break; -#ifdef ENABLE_WAYLAND - case OBS_NIX_PLATFORM_WAYLAND: { - QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); - gswindow.display = native->nativeResourceForWindow("surface", window); - success = gswindow.display != nullptr; - break; - } -#endif - default: - success = false; - break; - } -#endif - return success; -} - -OBSQTDisplay::OBSQTDisplay(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags) -{ - setAttribute(Qt::WA_PaintOnScreen); - setAttribute(Qt::WA_StaticContents); - setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_OpaquePaintEvent); - setAttribute(Qt::WA_DontCreateNativeAncestors); - setAttribute(Qt::WA_NativeWindow); - - auto windowVisible = [this](bool visible) { - if (!visible) { -#if !defined(_WIN32) && !defined(__APPLE__) - display = nullptr; -#endif - return; - } - - if (!display) { - CreateDisplay(); - } else { - QSize size = GetPixelSize(this); - obs_display_resize(display, size.width(), size.height()); - } - }; - - auto screenChanged = [this](QScreen *) { - CreateDisplay(); - - QSize size = GetPixelSize(this); - obs_display_resize(display, size.width(), size.height()); - }; - - connect(windowHandle(), &QWindow::visibleChanged, windowVisible); - connect(windowHandle(), &QWindow::screenChanged, screenChanged); - - windowHandle()->installEventFilter(new SurfaceEventFilter(this)); -} - -QColor OBSQTDisplay::GetDisplayBackgroundColor() const -{ - return rgba_to_color(backgroundColor); -} - -void OBSQTDisplay::SetDisplayBackgroundColor(const QColor &color) -{ - uint32_t newBackgroundColor = (uint32_t)color_to_int(color); - - if (newBackgroundColor != backgroundColor) { - backgroundColor = newBackgroundColor; - UpdateDisplayBackgroundColor(); - } -} - -void OBSQTDisplay::UpdateDisplayBackgroundColor() -{ - obs_display_set_background_color(display, backgroundColor); -} - -void OBSQTDisplay::CreateDisplay() -{ - if (display) - return; - - if (destroying) - return; - - if (!windowHandle()->isExposed()) - return; - - QSize size = GetPixelSize(this); - - gs_init_data info = {}; - info.cx = size.width(); - info.cy = size.height(); - info.format = GS_BGRA; - info.zsformat = GS_ZS_NONE; - - if (!QTToGSWindow(windowHandle(), info.window)) - return; - - display = obs_display_create(&info, backgroundColor); - - emit DisplayCreated(this); -} - -void OBSQTDisplay::paintEvent(QPaintEvent *event) -{ - CreateDisplay(); - - QWidget::paintEvent(event); -} - -void OBSQTDisplay::moveEvent(QMoveEvent *event) -{ - QWidget::moveEvent(event); - - OnMove(); -} - -bool OBSQTDisplay::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_DISPLAYCHANGE: - OnDisplayChange(); - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSQTDisplay::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - - CreateDisplay(); - - if (isVisible() && display) { - QSize size = GetPixelSize(this); - obs_display_resize(display, size.width(), size.height()); - } - - emit DisplayResized(); -} - -QPaintEngine *OBSQTDisplay::paintEngine() const -{ - return nullptr; -} - -void OBSQTDisplay::OnMove() -{ - if (display) - obs_display_update_color_space(display); -} - -void OBSQTDisplay::OnDisplayChange() -{ - if (display) - obs_display_update_color_space(display); -} diff --git a/frontend/utility/VolumeMeterTimer.cpp b/frontend/utility/VolumeMeterTimer.cpp index cd00c14d7..388b9640f 100644 --- a/frontend/utility/VolumeMeterTimer.cpp +++ b/frontend/utility/VolumeMeterTimer.cpp @@ -1,1354 +1,8 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolumeMeterTimer.hpp" -#include -#include -#include -#include -#include -#include -#include +#include -using namespace std; - -#define FADER_PRECISION 4096.0 - -// Size of the audio indicator in pixels -#define INDICATOR_THICKNESS 3 - -// Padding on top and bottom of vertical meters -#define METER_PADDING 1 - -std::weak_ptr VolumeMeter::updateTimer; - -static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) -{ - if (muted) - return Qt::Checked; - else if (unassigned) - return Qt::PartiallyChecked; - else - return Qt::Unchecked; -} - -static inline bool IsSourceUnassigned(obs_source_t *source) -{ - uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); - obs_monitoring_type mt = obs_source_get_monitoring_type(source); - - return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; -} - -static void ShowUnassignedWarning(const char *name) -{ - auto msgBox = [=]() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); - msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); -} - -void VolControl::OBSVolumeChanged(void *data, float db) -{ - Q_UNUSED(db); - VolControl *volControl = static_cast(data); - - QMetaObject::invokeMethod(volControl, "VolumeChanged"); -} - -void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - VolControl *volControl = static_cast(data); - - volControl->volMeter->setLevels(magnitude, peak, inputPeak); -} - -void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) -{ - VolControl *volControl = static_cast(data); - bool muted = calldata_bool(calldata, "muted"); - - QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); -} - -void VolControl::VolumeChanged() -{ - slider->blockSignals(true); - slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); - slider->blockSignals(false); - - updateText(); -} - -void VolControl::VolumeMuted(bool muted) -{ - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) -{ - - VolControl *volControl = static_cast(data); - QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); -} - -void VolControl::MixersOrMonitoringChanged() -{ - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::SetMuted(bool) -{ - bool checked = mute->checkState() == Qt::Checked; - bool prev = obs_source_muted(source); - obs_source_set_muted(source, checked); - bool unassigned = IsSourceUnassigned(source); - - if (!checked && unassigned) { - mute->setCheckState(Qt::PartiallyChecked); - /* Show notice about the source no being assigned to any tracks */ - bool has_shown_warning = - config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); - if (!has_shown_warning) - ShowUnassignedWarning(obs_source_get_name(source)); - } - - auto undo_redo = [](const std::string &uuid, bool val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_muted(source, val); - }; - - QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); - - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); -} - -void VolControl::SliderChanged(int vol) -{ - float prev = obs_source_get_volume(source); - - obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); - updateText(); - - auto undo_redo = [](const std::string &uuid, float val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_volume(source, val); - }; - - float val = obs_source_get_volume(source); - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), - std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); -} - -void VolControl::updateText() -{ - QString text; - float db = obs_fader_get_db(obs_fader); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - volLabel->setText(text); - - bool muted = obs_source_muted(source); - const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; - - QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName); - - slider->setAccessibleName(accText); -} - -void VolControl::EmitConfigClicked() -{ - emit ConfigClicked(); -} - -void VolControl::SetMeterDecayRate(qreal q) -{ - volMeter->setPeakDecayRate(q); -} - -void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - volMeter->setPeakMeterType(peakMeterType); -} - -VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) - : source(std::move(source_)), - levelTotal(0.0f), - levelCount(0.0f), - obs_fader(obs_fader_create(OBS_FADER_LOG)), - obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), - vertical(vertical), - contextMenu(nullptr) -{ - nameLabel = new OBSSourceLabel(source); - volLabel = new QLabel(); - mute = new MuteCheckBox(); - - volLabel->setObjectName("volLabel"); - volLabel->setAlignment(Qt::AlignCenter); - -#ifdef __APPLE__ - mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - QString sourceName = obs_source_get_name(source); - setObjectName(sourceName); - - if (showConfig) { - config = new QPushButton(this); - config->setProperty("class", "icon-dots-vert"); - config->setAutoDefault(false); - - config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); - - connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - } - - QVBoxLayout *mainLayout = new QVBoxLayout; - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - if (vertical) { - QHBoxLayout *nameLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QHBoxLayout *volLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QHBoxLayout *meterLayout = new QHBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new VolumeSlider(obs_fader, Qt::Vertical); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - nameLayout->setAlignment(Qt::AlignCenter); - meterLayout->setAlignment(Qt::AlignCenter); - controlLayout->setAlignment(Qt::AlignCenter); - volLayout->setAlignment(Qt::AlignCenter); - - meterFrame->setObjectName("volMeterFrame"); - - nameLayout->setContentsMargins(0, 0, 0, 0); - nameLayout->setSpacing(0); - nameLayout->addWidget(nameLabel); - - controlLayout->setContentsMargins(0, 0, 0, 0); - controlLayout->setSpacing(0); - - // Add Headphone (audio monitoring) widget here - controlLayout->addWidget(mute); - - if (showConfig) { - controlLayout->addWidget(config); - } - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - meterLayout->addWidget(slider); - meterLayout->addWidget(volMeter); - - meterFrame->setLayout(meterLayout); - - volLayout->setContentsMargins(0, 0, 0, 0); - volLayout->setSpacing(0); - volLayout->addWidget(volLabel); - volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); - - mainLayout->addItem(nameLayout); - mainLayout->addItem(volLayout); - mainLayout->addWidget(meterFrame); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - - // Default size can cause clipping of long names in vertical layout. - QFont font = nameLabel->font(); - QFontInfo info(font); - nameLabel->setFont(font); - - setMaximumWidth(110); - } else { - QHBoxLayout *textLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QVBoxLayout *meterLayout = new QVBoxLayout; - QVBoxLayout *buttonLayout = new QVBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - slider = new VolumeSlider(obs_fader, Qt::Horizontal); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - meterFrame->setObjectName("volMeterFrame"); - meterFrame->setLayout(meterLayout); - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - - meterLayout->addWidget(volMeter); - meterLayout->addWidget(slider); - - buttonLayout->setContentsMargins(0, 0, 0, 0); - buttonLayout->setSpacing(0); - - if (showConfig) { - buttonLayout->addWidget(config); - } - buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); - buttonLayout->addWidget(mute); - - controlLayout->addItem(buttonLayout); - controlLayout->addWidget(meterFrame); - - mainLayout->addItem(textLayout); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - } - - setLayout(mainLayout); - - nameLabel->setText(sourceName); - - slider->setMinimum(0); - slider->setMaximum(int(FADER_PRECISION)); - - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - mute->setCheckState(GetCheckState(muted, unassigned)); - volMeter->muted = muted || unassigned; - mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); - obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, - this); - - QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); - QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); - - obs_fader_attach_source(obs_fader, source); - obs_volmeter_attach_source(obs_volmeter, source); - - /* Call volume changed once to init the slider position and label */ - VolumeChanged(); -} - -void VolControl::EnableSlider(bool enable) -{ - slider->setEnabled(enable); -} - -VolControl::~VolControl() -{ - obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.clear(); - - if (contextMenu) - contextMenu->close(); -} - -static inline QColor color_from_int(long long val) -{ - QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); - color.setAlpha(255); - - return color; -} - -QColor VolumeMeter::getBackgroundNominalColor() const -{ - return p_backgroundNominalColor; -} - -QColor VolumeMeter::getBackgroundNominalColorDisabled() const -{ - return backgroundNominalColorDisabled; -} - -void VolumeMeter::setBackgroundNominalColor(QColor c) -{ - p_backgroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); - } else { - backgroundNominalColor = p_backgroundNominalColor; - } -} - -void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) -{ - backgroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundWarningColor() const -{ - return p_backgroundWarningColor; -} - -QColor VolumeMeter::getBackgroundWarningColorDisabled() const -{ - return backgroundWarningColorDisabled; -} - -void VolumeMeter::setBackgroundWarningColor(QColor c) -{ - p_backgroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); - } else { - backgroundWarningColor = p_backgroundWarningColor; - } -} - -void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) -{ - backgroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundErrorColor() const -{ - return p_backgroundErrorColor; -} - -QColor VolumeMeter::getBackgroundErrorColorDisabled() const -{ - return backgroundErrorColorDisabled; -} - -void VolumeMeter::setBackgroundErrorColor(QColor c) -{ - p_backgroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); - } else { - backgroundErrorColor = p_backgroundErrorColor; - } -} - -void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) -{ - backgroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundNominalColor() const -{ - return p_foregroundNominalColor; -} - -QColor VolumeMeter::getForegroundNominalColorDisabled() const -{ - return foregroundNominalColorDisabled; -} - -void VolumeMeter::setForegroundNominalColor(QColor c) -{ - p_foregroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); - } else { - foregroundNominalColor = p_foregroundNominalColor; - } -} - -void VolumeMeter::setForegroundNominalColorDisabled(QColor c) -{ - foregroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundWarningColor() const -{ - return p_foregroundWarningColor; -} - -QColor VolumeMeter::getForegroundWarningColorDisabled() const -{ - return foregroundWarningColorDisabled; -} - -void VolumeMeter::setForegroundWarningColor(QColor c) -{ - p_foregroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); - } else { - foregroundWarningColor = p_foregroundWarningColor; - } -} - -void VolumeMeter::setForegroundWarningColorDisabled(QColor c) -{ - foregroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundErrorColor() const -{ - return p_foregroundErrorColor; -} - -QColor VolumeMeter::getForegroundErrorColorDisabled() const -{ - return foregroundErrorColorDisabled; -} - -void VolumeMeter::setForegroundErrorColor(QColor c) -{ - p_foregroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); - } else { - foregroundErrorColor = p_foregroundErrorColor; - } -} - -void VolumeMeter::setForegroundErrorColorDisabled(QColor c) -{ - foregroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getClipColor() const -{ - return clipColor; -} - -void VolumeMeter::setClipColor(QColor c) -{ - clipColor = std::move(c); -} - -QColor VolumeMeter::getMagnitudeColor() const -{ - return magnitudeColor; -} - -void VolumeMeter::setMagnitudeColor(QColor c) -{ - magnitudeColor = std::move(c); -} - -QColor VolumeMeter::getMajorTickColor() const -{ - return majorTickColor; -} - -void VolumeMeter::setMajorTickColor(QColor c) -{ - majorTickColor = std::move(c); -} - -QColor VolumeMeter::getMinorTickColor() const -{ - return minorTickColor; -} - -void VolumeMeter::setMinorTickColor(QColor c) -{ - minorTickColor = std::move(c); -} - -int VolumeMeter::getMeterThickness() const -{ - return meterThickness; -} - -void VolumeMeter::setMeterThickness(int v) -{ - meterThickness = v; - recalculateLayout = true; -} - -qreal VolumeMeter::getMeterFontScaling() const -{ - return meterFontScaling; -} - -void VolumeMeter::setMeterFontScaling(qreal v) -{ - meterFontScaling = v; - recalculateLayout = true; -} - -void VolControl::refreshColors() -{ - volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); - volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); - volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); - volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); - volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); - volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); -} - -qreal VolumeMeter::getMinimumLevel() const -{ - return minimumLevel; -} - -void VolumeMeter::setMinimumLevel(qreal v) -{ - minimumLevel = v; -} - -qreal VolumeMeter::getWarningLevel() const -{ - return warningLevel; -} - -void VolumeMeter::setWarningLevel(qreal v) -{ - warningLevel = v; -} - -qreal VolumeMeter::getErrorLevel() const -{ - return errorLevel; -} - -void VolumeMeter::setErrorLevel(qreal v) -{ - errorLevel = v; -} - -qreal VolumeMeter::getClipLevel() const -{ - return clipLevel; -} - -void VolumeMeter::setClipLevel(qreal v) -{ - clipLevel = v; -} - -qreal VolumeMeter::getMinimumInputLevel() const -{ - return minimumInputLevel; -} - -void VolumeMeter::setMinimumInputLevel(qreal v) -{ - minimumInputLevel = v; -} - -qreal VolumeMeter::getPeakDecayRate() const -{ - return peakDecayRate; -} - -void VolumeMeter::setPeakDecayRate(qreal v) -{ - peakDecayRate = v; -} - -qreal VolumeMeter::getMagnitudeIntegrationTime() const -{ - return magnitudeIntegrationTime; -} - -void VolumeMeter::setMagnitudeIntegrationTime(qreal v) -{ - magnitudeIntegrationTime = v; -} - -qreal VolumeMeter::getPeakHoldDuration() const -{ - return peakHoldDuration; -} - -void VolumeMeter::setPeakHoldDuration(qreal v) -{ - peakHoldDuration = v; -} - -qreal VolumeMeter::getInputPeakHoldDuration() const -{ - return inputPeakHoldDuration; -} - -void VolumeMeter::setInputPeakHoldDuration(qreal v) -{ - inputPeakHoldDuration = v; -} - -void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); - switch (peakMeterType) { - case TRUE_PEAK_METER: - // For true-peak meters EBU has defined the Permitted Maximum, - // taking into account the accuracy of the meter and further - // processing required by lossy audio compression. - // - // The alignment level was not specified, but I've adjusted - // it compared to a sample-peak meter. Incidentally Youtube - // uses this new Alignment Level as the maximum integrated - // loudness of a video. - // - // * Permitted Maximum Level (PML) = -2.0 dBTP - // * Alignment Level (AL) = -13 dBTP - setErrorLevel(-2.0); - setWarningLevel(-13.0); - break; - - case SAMPLE_PEAK_METER: - default: - // For a sample Peak Meter EBU has the following level - // definitions, taking into account inaccuracies of this meter: - // - // * Permitted Maximum Level (PML) = -9.0 dBFS - // * Alignment Level (AL) = -20.0 dBFS - setErrorLevel(-9.0); - setWarningLevel(-20.0); - break; - } -} - -void VolumeMeter::mousePressEvent(QMouseEvent *event) -{ - setFocus(Qt::MouseFocusReason); - event->accept(); -} - -void VolumeMeter::wheelEvent(QWheelEvent *event) -{ - QApplication::sendEvent(focusProxy(), event); -} - -VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) - : QWidget(parent), - obs_volmeter(obs_volmeter), - vertical(vertical) -{ - setAttribute(Qt::WA_OpaquePaintEvent, true); - - // Default meter settings, they only show if - // there is no stylesheet, do not remove. - backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green - backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow - backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red - foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green - foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow - foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red - - backgroundNominalColorDisabled.setRgb(90, 90, 90); - backgroundWarningColorDisabled.setRgb(117, 117, 117); - backgroundErrorColorDisabled.setRgb(65, 65, 65); - foregroundNominalColorDisabled.setRgb(163, 163, 163); - foregroundWarningColorDisabled.setRgb(217, 217, 217); - foregroundErrorColorDisabled.setRgb(113, 113, 113); - - clipColor.setRgb(0xff, 0xff, 0xff); // Bright white - magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black - majorTickColor.setRgb(0x00, 0x00, 0x00); // Black - minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray - minimumLevel = -60.0; // -60 dB - warningLevel = -20.0; // -20 dB - errorLevel = -9.0; // -9 dB - clipLevel = -0.5; // -0.5 dB - minimumInputLevel = -50.0; // -50 dB - peakDecayRate = 11.76; // 20 dB / 1.7 sec - magnitudeIntegrationTime = 0.3; // 99% in 300 ms - peakHoldDuration = 20.0; // 20 seconds - inputPeakHoldDuration = 1.0; // 1 second - meterThickness = 3; // Bar thickness in pixels - meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size - channels = (int)audio_output_get_channels(obs_get_audio()); - - doLayout(); - updateTimerRef = updateTimer.lock(); - if (!updateTimerRef) { - updateTimerRef = std::make_shared(); - updateTimerRef->setTimerType(Qt::PreciseTimer); - updateTimerRef->start(16); - updateTimer = updateTimerRef; - } - - updateTimerRef->AddVolControl(this); -} - -VolumeMeter::~VolumeMeter() -{ - updateTimerRef->RemoveVolControl(this); -} - -void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - uint64_t ts = os_gettime_ns(); - QMutexLocker locker(&dataMutex); - - currentLastUpdateTime = ts; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = magnitude[channelNr]; - currentPeak[channelNr] = peak[channelNr]; - currentInputPeak[channelNr] = inputPeak[channelNr]; - } - - // In case there are more updates then redraws we must make sure - // that the ballistics of peak and hold are recalculated. - locker.unlock(); - calculateBallistics(ts); -} - -inline void VolumeMeter::resetLevels() -{ - currentLastUpdateTime = 0; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = -M_INFINITE; - currentPeak[channelNr] = -M_INFINITE; - currentInputPeak[channelNr] = -M_INFINITE; - - displayMagnitude[channelNr] = -M_INFINITE; - displayPeak[channelNr] = -M_INFINITE; - displayPeakHold[channelNr] = -M_INFINITE; - displayPeakHoldLastUpdateTime[channelNr] = 0; - displayInputPeakHold[channelNr] = -M_INFINITE; - displayInputPeakHoldLastUpdateTime[channelNr] = 0; - } -} - -bool VolumeMeter::needLayoutChange() -{ - int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); - - if (!currentNrAudioChannels) { - struct obs_audio_info oai; - obs_get_audio_info(&oai); - currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; - } - - if (displayNrAudioChannels != currentNrAudioChannels) { - displayNrAudioChannels = currentNrAudioChannels; - recalculateLayout = true; - } - - return recalculateLayout; -} - -// When this is called from the constructor, obs_volmeter_get_nr_channels has not -// yet been called and Q_PROPERTY settings have not yet been read from the -// stylesheet. -inline void VolumeMeter::doLayout() -{ - QMutexLocker locker(&dataMutex); - - if (displayNrAudioChannels) { - int meterSize = std::floor(22 / displayNrAudioChannels); - setMeterThickness(std::clamp(meterSize, 3, 7)); - } - recalculateLayout = false; - - tickFont = font(); - QFontInfo info(tickFont); - tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); - QFontMetrics metrics(tickFont); - if (vertical) { - // Each meter channel is meterThickness pixels wide, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, space to hold our longest label in this font, - // and a few pixels before the fader. - QRect scaleBounds = metrics.boundingRect("-88"); - setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); - } else { - // Each meter channel is meterThickness pixels high, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, and space high enough to hold our label in - // this font, presuming that digits don't have descenders. - setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); - } - - resetLevels(); -} - -inline bool VolumeMeter::detectIdle(uint64_t ts) -{ - double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; - if (timeSinceLastUpdate > 0.5) { - resetLevels(); - return true; - } else { - return false; - } -} - -inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) -{ - if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { - // Attack of peak is immediate. - displayPeak[channelNr] = currentPeak[channelNr]; - } else { - // Decay of peak is 40 dB / 1.7 seconds for Fast Profile - // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) - // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) - float decay = float(peakDecayRate * timeSinceLastRedraw); - displayPeak[channelNr] = - std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); - } - - if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak - // after 20 seconds. - qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > peakHoldDuration) { - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || - !isfinite(displayInputPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak after 1 second. - qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > inputPeakHoldDuration) { - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (!isfinite(displayMagnitude[channelNr])) { - // The statements in the else-leg do not work with - // NaN and infinite displayMagnitude. - displayMagnitude[channelNr] = currentMagnitude[channelNr]; - } else { - // A VU meter will integrate to the new value to 99% in 300 ms. - // The calculation here is very simplified and is more accurate - // with higher frame-rate. - float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * - (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); - displayMagnitude[channelNr] = - std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); - } -} - -inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) -{ - QMutexLocker locker(&dataMutex); - - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) - calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); -} - -void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) -{ - QMutexLocker locker(&dataMutex); - QColor color; - - if (peakHold < minimumInputLevel) - color = backgroundNominalColor; - else if (peakHold < warningLevel) - color = foregroundNominalColor; - else if (peakHold < errorLevel) - color = foregroundWarningColor; - else if (peakHold <= clipLevel) - color = foregroundErrorColor; - else - color = clipColor; - - painter.fillRect(x, y, width, height, color); -} - -void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) -{ - qreal scale = width / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = int(x + width - (i * scale) - 1); - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - QRect textBounds = metrics.boundingRect(str); - int pos; - if (i == 0) { - pos = position - textBounds.width(); - } else { - pos = position - (textBounds.width() / 2); - if (pos < 0) - pos = 0; - } - painter.drawText(pos, y + 4 + metrics.capHeight(), str); - - painter.drawLine(position, y, position, y + 2); - } -} - -void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) -{ - qreal scale = height / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = y + int(i * scale) + METER_PADDING; - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - if (i == 0) { - painter.drawText(x + 10, position + metrics.capHeight(), str); - } else { - painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); - } - - painter.drawLine(x, position, x + 2, position); - } -} - -#define CLIP_FLASH_DURATION_MS 1000 - -inline int VolumeMeter::convertToInt(float number) -{ - constexpr int min = std::numeric_limits::min(); - constexpr int max = std::numeric_limits::max(); - - // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 - if (number >= (float)max) - return max; - else if (number < min) - return min; - else - return int(number); -} - -void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = width / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = x + 0; - int maximumPosition = x + width; - int magnitudePosition = x + width - convertToInt(magnitude * scale); - int peakPosition = x + width - convertToInt(peak * scale); - int peakHoldPosition = x + width - convertToInt(peakHold * scale); - int warningPosition = x + width - convertToInt(warningLevel * scale); - int errorPosition = x + width - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(minimumPosition, y, end, height, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); -} - -void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = height / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = y + 0; - int maximumPosition = y + height; - int magnitudePosition = y + height - convertToInt(magnitude * scale); - int peakPosition = y + height - convertToInt(peak * scale); - int peakHoldPosition = y + height - convertToInt(peakHold * scale); - int warningPosition = y + height - convertToInt(warningLevel * scale); - int errorPosition = y + height - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(x, minimumPosition, width, end, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); -} - -void VolumeMeter::paintEvent(QPaintEvent *event) -{ - uint64_t ts = os_gettime_ns(); - qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; - calculateBallistics(ts, timeSinceLastRedraw); - bool idle = detectIdle(ts); - - QRect widgetRect = rect(); - int width = widgetRect.width(); - int height = widgetRect.height(); - - QPainter painter(this); - - // Paint window background color (as widget is opaque) - QColor background = palette().color(QPalette::ColorRole::Window); - painter.fillRect(event->region().boundingRect(), background); - - if (vertical) - height -= METER_PADDING * 2; - - // timerEvent requests update of the bar(s) only, so we can avoid the - // overhead of repainting the scale and labels. - if (event->region().boundingRect() != getBarRect()) { - if (needLayoutChange()) - doLayout(); - - if (vertical) { - paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, - height - (INDICATOR_THICKNESS + 3)); - } else { - paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, - width - (INDICATOR_THICKNESS + 3)); - } - } - - if (vertical) { - // Invert the Y axis to ease the math - painter.translate(0, height + METER_PADDING); - painter.scale(1, -1); - } - - for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { - - int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; - - if (vertical) - paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, - height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - else - paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), - width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - - if (idle) - continue; - - // By not drawing the input meter boxes the user can - // see that the audio stream has been stopped, without - // having too much visual impact. - if (vertical) - paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, - INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); - else - paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, - meterThickness, displayInputPeakHold[channelNrFixed]); - } - - lastRedrawTime = ts; -} - -QRect VolumeMeter::getBarRect() const -{ - QRect rec = rect(); - if (vertical) - rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); - else - rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); - - return rec; -} - -void VolumeMeter::changeEvent(QEvent *e) -{ - if (e->type() == QEvent::StyleChange) - recalculateLayout = true; - - QWidget::changeEvent(e); -} +#include "moc_VolumeMeterTimer.cpp" void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) { @@ -1372,136 +26,3 @@ void VolumeMeterTimer::timerEvent(QTimerEvent *) } } } - -VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) -{ - fad = fader; -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) - : AbsoluteSlider(orientation, parent) -{ - fad = fader; -} - -bool VolumeSlider::getDisplayTicks() const -{ - return displayTicks; -} - -void VolumeSlider::setDisplayTicks(bool display) -{ - displayTicks = display; -} - -void VolumeSlider::paintEvent(QPaintEvent *event) -{ - if (!getDisplayTicks()) { - QSlider::paintEvent(event); - return; - } - - QPainter painter(this); - QColor tickColor(91, 98, 115, 255); - - obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); - - QStyleOptionSlider opt; - initStyleOption(&opt); - - QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - if (orientation() == Qt::Horizontal) { - const int sliderWidth = groove.width() - handle.width(); - - float tickLength = groove.height() * 1.5; - tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); - - float yPos = groove.center().y() - (tickLength / 2) + 1; - - for (int db = -10; db >= -90; db -= 10) { - float tickValue = fader_db_to_def(db); - - float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); - painter.fillRect(xPos, yPos, 1, tickLength, tickColor); - } - } - - if (orientation() == Qt::Vertical) { - const int sliderHeight = groove.height() - handle.height(); - - float tickLength = groove.width() * 1.5; - tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); - - float xPos = groove.center().x() - (tickLength / 2) + 1; - - for (int db = -10; db >= -96; db -= 10) { - float tickValue = fader_db_to_def(db); - - float yPos = - groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); - painter.fillRect(xPos, yPos, tickLength, 1, tickColor); - } - } - - QSlider::paintEvent(event); -} - -VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} - -VolumeSlider *VolumeAccessibleInterface::slider() const -{ - return qobject_cast(object()); -} - -QString VolumeAccessibleInterface::text(QAccessible::Text t) const -{ - if (slider()->isVisible()) { - switch (t) { - case QAccessible::Text::Value: - return currentValue().toString(); - default: - break; - } - } - return QAccessibleWidget::text(t); -} - -QVariant VolumeAccessibleInterface::currentValue() const -{ - QString text; - float db = obs_fader_get_db(slider()->fad); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - return text; -} - -void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) -{ - slider()->setValue(value.toInt()); -} - -QVariant VolumeAccessibleInterface::maximumValue() const -{ - return slider()->maximum(); -} - -QVariant VolumeAccessibleInterface::minimumValue() const -{ - return slider()->minimum(); -} - -QVariant VolumeAccessibleInterface::minimumStepSize() const -{ - return slider()->singleStep(); -} - -QAccessible::Role VolumeAccessibleInterface::role() const -{ - return QAccessible::Role::Slider; -} diff --git a/frontend/utility/VolumeMeterTimer.hpp b/frontend/utility/VolumeMeterTimer.hpp index 51c77f247..13590cb8f 100644 --- a/frontend/utility/VolumeMeterTimer.hpp +++ b/frontend/utility/VolumeMeterTimer.hpp @@ -1,230 +1,8 @@ #pragma once -#include -#include -#include #include -#include -#include -#include -#include -#include "absolute-slider.hpp" -class QPushButton; -class VolumeMeterTimer; -class VolumeSlider; - -class VolumeMeter : public QWidget { - Q_OBJECT - Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) - - Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE - setBackgroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE - setBackgroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE - setBackgroundErrorColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE - setForegroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE - setForegroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE - setForegroundErrorColorDisabled DESIGNABLE true) - - Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) - Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) - Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) - Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) - Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) - Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) - - // Levels are denoted in dBFS. - Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) - Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) - Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) - Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) - Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) - - // Rates are denoted in dB/second. - Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) - - // Time in seconds for the VU meter to integrate over. - Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime - DESIGNABLE true) - - // Duration is denoted in seconds. - Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) - Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration - DESIGNABLE true) - - friend class VolControl; - -private: - obs_volmeter_t *obs_volmeter; - static std::weak_ptr updateTimer; - std::shared_ptr updateTimerRef; - - inline void resetLevels(); - inline void doLayout(); - inline bool detectIdle(uint64_t ts); - inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); - inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); - - inline int convertToInt(float number); - void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); - void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintHTicks(QPainter &painter, int x, int y, int width); - void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintVTicks(QPainter &painter, int x, int y, int height); - - QMutex dataMutex; - - bool recalculateLayout = true; - uint64_t currentLastUpdateTime = 0; - float currentMagnitude[MAX_AUDIO_CHANNELS]; - float currentPeak[MAX_AUDIO_CHANNELS]; - float currentInputPeak[MAX_AUDIO_CHANNELS]; - - int displayNrAudioChannels = 0; - float displayMagnitude[MAX_AUDIO_CHANNELS]; - float displayPeak[MAX_AUDIO_CHANNELS]; - float displayPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - float displayInputPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - - QFont tickFont; - QColor backgroundNominalColor; - QColor backgroundWarningColor; - QColor backgroundErrorColor; - QColor foregroundNominalColor; - QColor foregroundWarningColor; - QColor foregroundErrorColor; - - QColor backgroundNominalColorDisabled; - QColor backgroundWarningColorDisabled; - QColor backgroundErrorColorDisabled; - QColor foregroundNominalColorDisabled; - QColor foregroundWarningColorDisabled; - QColor foregroundErrorColorDisabled; - - QColor clipColor; - QColor magnitudeColor; - QColor majorTickColor; - QColor minorTickColor; - - int meterThickness; - qreal meterFontScaling; - - qreal minimumLevel; - qreal warningLevel; - qreal errorLevel; - qreal clipLevel; - qreal minimumInputLevel; - qreal peakDecayRate; - qreal magnitudeIntegrationTime; - qreal peakHoldDuration; - qreal inputPeakHoldDuration; - - QColor p_backgroundNominalColor; - QColor p_backgroundWarningColor; - QColor p_backgroundErrorColor; - QColor p_foregroundNominalColor; - QColor p_foregroundWarningColor; - QColor p_foregroundErrorColor; - - uint64_t lastRedrawTime = 0; - int channels = 0; - bool clipping = false; - bool vertical; - bool muted = false; - -public: - explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); - ~VolumeMeter(); - - void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]); - QRect getBarRect() const; - bool needLayoutChange(); - - QColor getBackgroundNominalColor() const; - void setBackgroundNominalColor(QColor c); - QColor getBackgroundWarningColor() const; - void setBackgroundWarningColor(QColor c); - QColor getBackgroundErrorColor() const; - void setBackgroundErrorColor(QColor c); - QColor getForegroundNominalColor() const; - void setForegroundNominalColor(QColor c); - QColor getForegroundWarningColor() const; - void setForegroundWarningColor(QColor c); - QColor getForegroundErrorColor() const; - void setForegroundErrorColor(QColor c); - - QColor getBackgroundNominalColorDisabled() const; - void setBackgroundNominalColorDisabled(QColor c); - QColor getBackgroundWarningColorDisabled() const; - void setBackgroundWarningColorDisabled(QColor c); - QColor getBackgroundErrorColorDisabled() const; - void setBackgroundErrorColorDisabled(QColor c); - QColor getForegroundNominalColorDisabled() const; - void setForegroundNominalColorDisabled(QColor c); - QColor getForegroundWarningColorDisabled() const; - void setForegroundWarningColorDisabled(QColor c); - QColor getForegroundErrorColorDisabled() const; - void setForegroundErrorColorDisabled(QColor c); - - QColor getClipColor() const; - void setClipColor(QColor c); - QColor getMagnitudeColor() const; - void setMagnitudeColor(QColor c); - QColor getMajorTickColor() const; - void setMajorTickColor(QColor c); - QColor getMinorTickColor() const; - void setMinorTickColor(QColor c); - int getMeterThickness() const; - void setMeterThickness(int v); - qreal getMeterFontScaling() const; - void setMeterFontScaling(qreal v); - qreal getMinimumLevel() const; - void setMinimumLevel(qreal v); - qreal getWarningLevel() const; - void setWarningLevel(qreal v); - qreal getErrorLevel() const; - void setErrorLevel(qreal v); - qreal getClipLevel() const; - void setClipLevel(qreal v); - qreal getMinimumInputLevel() const; - void setMinimumInputLevel(qreal v); - qreal getPeakDecayRate() const; - void setPeakDecayRate(qreal v); - qreal getMagnitudeIntegrationTime() const; - void setMagnitudeIntegrationTime(qreal v); - qreal getPeakHoldDuration() const; - void setPeakHoldDuration(qreal v); - qreal getInputPeakHoldDuration() const; - void setInputPeakHoldDuration(qreal v); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void wheelEvent(QWheelEvent *event) override; - -protected: - void paintEvent(QPaintEvent *event) override; - void changeEvent(QEvent *e) override; -}; +class VolumeMeter; class VolumeMeterTimer : public QTimer { Q_OBJECT @@ -239,103 +17,3 @@ protected: void timerEvent(QTimerEvent *event) override; QList volumeMeters; }; - -class QLabel; -class VolumeSlider; -class MuteCheckBox; -class OBSSourceLabel; - -class VolControl : public QFrame { - Q_OBJECT - -private: - OBSSource source; - std::vector sigs; - OBSSourceLabel *nameLabel; - QLabel *volLabel; - VolumeMeter *volMeter; - VolumeSlider *slider; - MuteCheckBox *mute; - QPushButton *config = nullptr; - float levelTotal; - float levelCount; - OBSFader obs_fader; - OBSVolMeter obs_volmeter; - bool vertical; - QMenu *contextMenu; - - static void OBSVolumeChanged(void *param, float db); - static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); - static void OBSVolumeMuted(void *data, calldata_t *calldata); - static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); - - void EmitConfigClicked(); - -private slots: - void VolumeChanged(); - void VolumeMuted(bool muted); - void MixersOrMonitoringChanged(); - - void SetMuted(bool checked); - void SliderChanged(int vol); - void updateText(); - -signals: - void ConfigClicked(); - -public: - explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); - ~VolControl(); - - inline obs_source_t *GetSource() const { return source; } - - void SetMeterDecayRate(qreal q); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - - void EnableSlider(bool enable); - inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } - - void refreshColors(); -}; - -class VolumeSlider : public AbsoluteSlider { - Q_OBJECT - -public: - obs_fader_t *fad; - - VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); - VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); - - bool getDisplayTicks() const; - void setDisplayTicks(bool display); - -private: - bool displayTicks = false; - QColor tickColor; - -protected: - virtual void paintEvent(QPaintEvent *event) override; -}; - -class VolumeAccessibleInterface : public QAccessibleWidget { - -public: - VolumeAccessibleInterface(QWidget *w); - - QVariant currentValue() const; - void setCurrentValue(const QVariant &value); - - QVariant maximumValue() const; - QVariant minimumValue() const; - - QVariant minimumStepSize() const; - -private: - VolumeSlider *slider() const; - -protected: - virtual QAccessible::Role role() const override; - virtual QString text(QAccessible::Text t) const override; -}; diff --git a/frontend/widgets/ColorSelect.cpp b/frontend/widgets/ColorSelect.cpp index 47cac8dcc..297b578b2 100644 --- a/frontend/widgets/ColorSelect.cpp +++ b/frontend/widgets/ColorSelect.cpp @@ -16,10188 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} +#include "ColorSelect.hpp" ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) { ui->setupUi(this); } - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/ColorSelect.hpp b/frontend/widgets/ColorSelect.hpp index 81bb7b478..eab213409 100644 --- a/frontend/widgets/ColorSelect.hpp +++ b/frontend/widgets/ColorSelect.hpp @@ -17,153 +17,9 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "window-main.hpp" -#include "window-basic-interaction.hpp" -#include "window-basic-vcam.hpp" -#include "window-basic-properties.hpp" -#include "window-basic-transform.hpp" -#include "window-basic-adv-audio.hpp" -#include "window-basic-filters.hpp" -#include "window-missing-files.hpp" -#include "window-projector.hpp" -#include "window-basic-about.hpp" -#ifdef YOUTUBE_ENABLED -#include "window-dock-youtube-app.hpp" -#endif -#include "auth-base.hpp" -#include "log-viewer.hpp" -#include "undo-stack-obs.hpp" - -#include - -#include -#include -#include - -#include - -class QMessageBox; -class QListWidgetItem; -class VolControl; -class OBSBasicStats; -class OBSBasicVCamConfig; - -#include "ui_OBSBasic.h" #include "ui_ColorSelect.h" -#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") -#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") -#define AUX_AUDIO_1 Str("AuxAudioDevice1") -#define AUX_AUDIO_2 Str("AuxAudioDevice2") -#define AUX_AUDIO_3 Str("AuxAudioDevice3") -#define AUX_AUDIO_4 Str("AuxAudioDevice4") - -#define SIMPLE_ENCODER_X264 "x264" -#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" -#define SIMPLE_ENCODER_QSV "qsv" -#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" -#define SIMPLE_ENCODER_NVENC "nvenc" -#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" -#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" -#define SIMPLE_ENCODER_AMD "amd" -#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" -#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" -#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" -#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" - -#define PREVIEW_EDGE_SIZE 10 - -struct BasicOutputHandler; - -enum class QtDataRole { - OBSRef = Qt::UserRole, - OBSSignals, -}; - -struct SavedProjectorInfo { - ProjectorType type; - int monitor; - std::string geometry; - std::string name; - bool alwaysOnTop; - bool alwaysOnTopOverridden; -}; - -struct SourceCopyInfo { - OBSWeakSource weak_source; - bool visible; - obs_sceneitem_crop crop; - obs_transform_info transform; - obs_blending_method blend_method; - obs_blending_type blend_mode; -}; - -struct QuickTransition { - QPushButton *button = nullptr; - OBSSource source; - obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; - int duration = 0; - int id = 0; - bool fadeToBlack = false; - - inline QuickTransition() {} - inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) - : source(source_), - duration(duration_), - id(id_), - fadeToBlack(fadeToBlack_), - renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", - SourceRenamed, this)) - { - } - -private: - static void SourceRenamed(void *param, calldata_t *data); - std::shared_ptr renamedSignal; -}; - -struct OBSProfile { - std::string name; - std::string directoryName; - std::filesystem::path path; - std::filesystem::path profileFile; -}; - -struct OBSSceneCollection { - std::string name; - std::string fileName; - std::filesystem::path collectionFile; -}; - -struct OBSPromptResult { - bool success; - std::string promptValue; - bool optionValue; -}; - -struct OBSPromptRequest { - std::string title; - std::string prompt; - std::string promptValue; - bool withOption; - std::string optionPrompt; - bool optionValue; -}; - -using OBSPromptCallback = std::function; - -using OBSProfileCache = std::map; -using OBSSceneCollectionCache = std::map; +#include class ColorSelect : public QWidget { @@ -173,1210 +29,3 @@ public: private: std::unique_ptr ui; }; - -class OBSBasic : public OBSMainWindow { - Q_OBJECT - Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) - Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) - Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) - Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) - Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) - Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) - Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) - Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) - Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) - Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) - Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) - Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon - DESIGNABLE true) - - friend class OBSAbout; - friend class OBSBasicPreview; - friend class OBSBasicStatusBar; - friend class OBSBasicSourceSelect; - friend class OBSBasicTransform; - friend class OBSBasicSettings; - friend class Auth; - friend class AutoConfig; - friend class AutoConfigStreamPage; - friend class RecordButton; - friend class ControlsSplitButton; - friend class ExtraBrowsersModel; - friend class ExtraBrowsersDelegate; - friend class DeviceCaptureToolbar; - friend class OBSBasicSourceSelect; - friend class OBSYoutubeActions; - friend class OBSPermissions; - friend struct BasicOutputHandler; - friend struct OBSStudioAPI; - friend class ScreenshotObj; - - enum class MoveDir { Up, Down, Left, Right }; - - enum DropType { - DropType_RawText, - DropType_Text, - DropType_Image, - DropType_Media, - DropType_Html, - DropType_Url, - }; - - enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; - - enum class CenterType { - Scene, - Vertical, - Horizontal, - }; - -private: - obs_frontend_callbacks *api = nullptr; - - std::shared_ptr auth; - - std::vector volumes; - - std::vector signalHandlers; - - QList> oldExtraDocks; - QStringList oldExtraDockNames; - - OBSDataAutoRelease collectionModuleData; - std::vector safeModeTransitions; - - bool loaded = false; - long disableSaving = 1; - bool projectChanged = false; - bool previewEnabled = true; - ContextBarSize contextBarSize = ContextBarSize_Normal; - - std::deque clipboard; - OBSWeakSourceAutoRelease copyFiltersSource; - bool copyVisible = true; - obs_transform_info copiedTransformInfo; - obs_sceneitem_crop copiedCropInfo; - bool hasCopiedTransform = false; - OBSWeakSourceAutoRelease copySourceTransition; - int copySourceTransitionDuration; - - bool closing = false; - bool clearingFailed = false; - - QScopedPointer devicePropertiesThread; - QScopedPointer whatsNewInitThread; - QScopedPointer updateCheckThread; - QScopedPointer introCheckThread; - QScopedPointer logUploadThread; - - QPointer interaction; - QPointer properties; - QPointer transformWindow; - QPointer advAudioWindow; - QPointer filters; - QPointer statsDock; -#ifdef YOUTUBE_ENABLED - QPointer youtubeAppDock; - uint64_t lastYouTubeAppDockCreationTime = 0; -#endif - QPointer about; - QPointer missDialog; - QPointer logView; - - QPointer cpuUsageTimer; - QPointer diskFullTimer; - - QPointer nudge_timer; - bool recent_nudge = false; - - os_cpu_usage_info_t *cpuUsageInfo = nullptr; - - OBSService service; - std::unique_ptr outputHandler; - std::shared_future setupStreamingGuard; - bool streamingStopping = false; - bool recordingStopping = false; - bool replayBufferStopping = false; - - gs_vertbuffer_t *box = nullptr; - gs_vertbuffer_t *boxLeft = nullptr; - gs_vertbuffer_t *boxTop = nullptr; - gs_vertbuffer_t *boxRight = nullptr; - gs_vertbuffer_t *boxBottom = nullptr; - gs_vertbuffer_t *circle = nullptr; - - gs_vertbuffer_t *actionSafeMargin = nullptr; - gs_vertbuffer_t *graphicsSafeMargin = nullptr; - gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; - - int previewX = 0, previewY = 0; - int previewCX = 0, previewCY = 0; - float previewScale = 0.0f; - - ConfigFile activeConfiguration; - - std::vector savedProjectorsArray; - std::vector projectors; - - QPointer stats; - QPointer remux; - QPointer extraBrowsers; - QPointer importer; - - QPointer transitionButton; - - bool vcamEnabled = false; - VCamConfig vcamConfig; - - QScopedPointer trayIcon; - QPointer sysTrayStream; - QPointer sysTrayRecord; - QPointer sysTrayReplayBuffer; - QPointer sysTrayVirtualCam; - QPointer showHide; - QPointer exit; - QPointer trayMenu; - QPointer previewProjector; - QPointer studioProgramProjector; - QPointer previewProjectorSource; - QPointer previewProjectorMain; - QPointer sceneProjectorMenu; - QPointer sourceProjector; - QPointer scaleFilteringMenu; - QPointer blendingMethodMenu; - QPointer blendingModeMenu; - QPointer colorMenu; - QPointer colorWidgetAction; - QPointer colorSelect; - QPointer deinterlaceMenu; - QPointer perSceneTransitionMenu; - QPointer shortcutFilter; - QPointer renameScene; - QPointer renameSource; - - QPointer programWidget; - QPointer programLayout; - QPointer programLabel; - - QScopedPointer patronJsonThread; - std::string patronJson; - - std::atomic currentScene = nullptr; - std::optional> lastOutputResolution; - std::optional> migrationBaseResolution; - bool usingAbsoluteCoordinates = false; - - void DisableRelativeCoordinates(bool disable); - - void OnEvent(enum obs_frontend_event event); - - void UpdateMultiviewProjectorMenu(); - - void DrawBackdrop(float cx, float cy); - - void SetupEncoders(); - - void CreateFirstRunSources(); - void CreateDefaultScene(bool firstStart); - - void UpdateVolumeControlsDecayRate(); - void UpdateVolumeControlsPeakMeterType(); - void ClearVolumeControls(); - - void UploadLog(const char *subdir, const char *file, const bool crash); - - void Save(const char *file); - void LoadData(obs_data_t *data, const char *file, bool remigrate = false); - void Load(const char *file, bool remigrate = false); - - void InitHotkeys(); - void CreateHotkeys(); - void ClearHotkeys(); - - bool InitService(); - - bool InitBasicConfigDefaults(); - void InitBasicConfigDefaults2(); - bool InitBasicConfig(); - - void InitOBSCallbacks(); - - void InitPrimitives(); - - void OnFirstLoad(); - - OBSSceneItem GetSceneItem(QListWidgetItem *item); - OBSSceneItem GetCurrentSceneItem(); - - bool QueryRemoveSource(obs_source_t *source); - - void TimedCheckForUpdates(); - void CheckForUpdates(bool manualUpdate); - - void GetFPSCommon(uint32_t &num, uint32_t &den) const; - void GetFPSInteger(uint32_t &num, uint32_t &den) const; - void GetFPSFraction(uint32_t &num, uint32_t &den) const; - void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; - void GetConfigFPS(uint32_t &num, uint32_t &den) const; - - void UpdatePreviewScalingMenu(); - - void LoadSceneListOrder(obs_data_array_t *array); - obs_data_array_t *SaveSceneListOrder(); - void ChangeSceneIndex(bool relative, int idx, int invalidIdx); - - void TempFileOutput(const char *path, int vBitrate, int aBitrate); - void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); - - void CloseDialogs(); - void ClearSceneData(); - void ClearProjectors(); - - void Nudge(int dist, MoveDir dir); - - OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); - - void GetAudioSourceFilters(); - void GetAudioSourceProperties(); - void VolControlContextMenu(); - void ToggleVolControlLayout(); - void ToggleMixerLayout(bool vertical); - - void LogScenes(); - void SaveProjectNow(); - - int GetTopSelectedSourceItem(); - - QModelIndexList GetAllSelectedSourceItems(); - - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, - togglePreviewHotkeys, contextBarHotkeys; - obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; - - void InitDefaultTransitions(); - void InitTransition(obs_source_t *transition); - obs_source_t *FindTransition(const char *name); - OBSSource GetCurrentTransition(); - obs_data_array_t *SaveTransitions(); - void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); - - obs_source_t *fadeTransition; - obs_source_t *cutTransition; - - void CreateProgramDisplay(); - void CreateProgramOptions(); - void AddQuickTransitionId(int id); - void AddQuickTransition(); - void AddQuickTransitionHotkey(QuickTransition *qt); - void RemoveQuickTransitionHotkey(QuickTransition *qt); - void LoadQuickTransitions(obs_data_array_t *array); - obs_data_array_t *SaveQuickTransitions(); - void ClearQuickTransitionWidgets(); - void RefreshQuickTransitions(); - void DisableQuickTransitionWidgets(); - void EnableTransitionWidgets(bool enable); - void CreateDefaultQuickTransitions(); - - void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); - QMenu *CreatePerSceneTransitionMenu(); - QMenu *CreateVisibilityTransitionMenu(bool visible); - - QuickTransition *GetQuickTransition(int id); - int GetQuickTransitionIdx(int id); - QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); - void ClearQuickTransitions(); - void QuickTransitionClicked(); - void QuickTransitionChange(); - void QuickTransitionChangeDuration(int value); - void QuickTransitionRemoveClicked(); - - void SetPreviewProgramMode(bool enabled); - void ResizeProgram(uint32_t cx, uint32_t cy); - void SetCurrentScene(obs_scene_t *scene, bool force = false); - static void RenderProgram(void *data, uint32_t cx, uint32_t cy); - - std::vector quickTransitions; - QPointer programOptions; - QPointer program; - OBSWeakSource lastScene; - OBSWeakSource swapScene; - OBSWeakSource programScene; - OBSWeakSource lastProgramScene; - bool editPropertiesMode = false; - bool sceneDuplicationMode = true; - bool swapScenesMode = true; - volatile bool previewProgramMode = false; - obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; - obs_hotkey_id transitionHotkey = 0; - obs_hotkey_id statsHotkey = 0; - obs_hotkey_id screenshotHotkey = 0; - obs_hotkey_id sourceScreenshotHotkey = 0; - int quickTransitionIdCounter = 1; - bool overridingTransition = false; - - int programX = 0, programY = 0; - int programCX = 0, programCY = 0; - float programScale = 0.0f; - - int disableOutputsRef = 0; - - inline void OnActivate(bool force = false); - inline void OnDeactivate(); - - void AddDropSource(const char *file, DropType image); - void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - - bool sysTrayMinimizeToTray(); - - void EnumDialogs(); - - QList visDialogs; - QList modalDialogs; - QList visMsgBoxes; - - QList visDlgPositions; - - QByteArray startingDockLayout; - - obs_data_array_t *SaveProjectors(); - void LoadSavedProjectors(obs_data_array_t *savedProjectors); - - void MacBranchesFetched(const QString &branch, bool manualUpdate); - void ReceivedIntroJson(const QString &text); - void ShowWhatsNew(const QString &url); - - void UpdatePreviewProgramIndicators(); - - QStringList extraDockNames; - QList> extraDocks; - - QStringList extraCustomDockNames; - QList> extraCustomDocks; - -#ifdef BROWSER_AVAILABLE - QPointer extraBrowserMenuDocksSeparator; - - QList> extraBrowserDocks; - QStringList extraBrowserDockNames; - QStringList extraBrowserDockTargets; - - void ClearExtraBrowserDocks(); - void LoadExtraBrowserDocks(); - void SaveExtraBrowserDocks(); - void ManageExtraBrowserDocks(); - void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); -#endif - - QIcon imageIcon; - QIcon colorIcon; - QIcon slideshowIcon; - QIcon audioInputIcon; - QIcon audioOutputIcon; - QIcon desktopCapIcon; - QIcon windowCapIcon; - QIcon gameCapIcon; - QIcon cameraIcon; - QIcon textIcon; - QIcon mediaIcon; - QIcon browserIcon; - QIcon groupIcon; - QIcon sceneIcon; - QIcon defaultIcon; - QIcon audioProcessOutputIcon; - - QIcon GetImageIcon() const; - QIcon GetColorIcon() const; - QIcon GetSlideshowIcon() const; - QIcon GetAudioInputIcon() const; - QIcon GetAudioOutputIcon() const; - QIcon GetDesktopCapIcon() const; - QIcon GetWindowCapIcon() const; - QIcon GetGameCapIcon() const; - QIcon GetCameraIcon() const; - QIcon GetTextIcon() const; - QIcon GetMediaIcon() const; - QIcon GetBrowserIcon() const; - QIcon GetDefaultIcon() const; - QIcon GetAudioProcessOutputIcon() const; - - QSlider *tBar; - bool tBarActive = false; - - OBSSource GetOverrideTransition(OBSSource source); - int GetOverrideTransitionDuration(OBSSource source); - - void UpdateProjectorHideCursor(); - void UpdateProjectorAlwaysOnTop(bool top); - void ResetProjectors(); - - QPointer screenshotData; - - void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); - - void UpdatePreviewSafeAreas(); - bool drawSafeAreas = false; - - void CenterSelectedSceneItems(const CenterType ¢erType); - void ShowMissingFilesDialog(obs_missing_files_t *files); - - QColor selectionColor; - QColor cropColor; - QColor hoverColor; - - QColor GetCropColor() const; - QColor GetHoverColor() const; - - void UpdatePreviewSpacingHelpers(); - bool drawSpacingHelpers = true; - - float GetDevicePixelRatio(); - void SourceToolBarActionsSetEnabled(); - - std::string lastScreenshot; - std::string lastReplay; - - void UpdatePreviewOverflowSettings(); - void UpdatePreviewScrollbars(); - - bool streamingStarting = false; - - bool recordingStarted = false; - bool isRecordingPausable = false; - bool recordingPaused = false; - - bool restartingVCam = false; - -public slots: - void DeferSaveBegin(); - void DeferSaveEnd(); - - void DisplayStreamStartError(); - - void SetupBroadcast(); - - void StartStreaming(); - void StopStreaming(); - void ForceStopStreaming(); - - void StreamDelayStarting(int sec); - void StreamDelayStopping(int sec); - - void StreamingStart(); - void StreamStopping(); - void StreamingStop(int errorcode, QString last_error); - - void StartRecording(); - void StopRecording(); - - void RecordingStart(); - void RecordStopping(); - void RecordingStop(int code, QString last_error); - void RecordingFileChanged(QString lastRecordingPath); - - void ShowReplayBufferPauseWarning(); - void StartReplayBuffer(); - void StopReplayBuffer(); - - void ReplayBufferStart(); - void ReplayBufferSave(); - void ReplayBufferSaved(); - void ReplayBufferStopping(); - void ReplayBufferStop(int code); - - void StartVirtualCam(); - void StopVirtualCam(); - - void OnVirtualCamStart(); - void OnVirtualCamStop(int code); - - void SaveProjectDeferred(); - void SaveProject(); - - void SetTransition(OBSSource transition); - void OverrideTransition(OBSSource transition); - void TransitionToScene(OBSScene scene, bool force = false); - void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, - bool black = false, bool manual = false); - void SetCurrentScene(OBSSource scene, bool force = false); - - void UpdatePatronJson(const QString &text, const QString &error); - - void ShowContextBar(); - void HideContextBar(); - void PauseRecording(); - void UnpauseRecording(); - - void UpdateEditMenu(); - -private slots: - - void on_actionMainUndo_triggered(); - void on_actionMainRedo_triggered(); - - void AddSceneItem(OBSSceneItem item); - void AddScene(OBSSource source); - void RemoveScene(OBSSource source); - void RenameSources(OBSSource source, QString newName, QString prevName); - - void ActivateAudioSource(OBSSource source); - void DeactivateAudioSource(OBSSource source); - - void DuplicateSelectedScene(); - void RemoveSelectedScene(); - - void ToggleAlwaysOnTop(); - - void ReorderSources(OBSScene scene); - void RefreshSources(OBSScene scene); - - void ProcessHotkey(obs_hotkey_id id, bool pressed); - - void AddTransition(const char *id); - void RenameTransition(OBSSource transition); - void TransitionClicked(); - void TransitionStopped(); - void TransitionFullyStopped(); - void TriggerQuickTransition(int id); - - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - - void IconActivated(QSystemTrayIcon::ActivationReason reason); - void SetShowing(bool showing); - - void ToggleShowHide(); - - void HideAudioControl(); - void UnhideAllAudioControls(); - void ToggleHideMixer(); - - void MixerRenameSource(); - - void on_vMixerScrollArea_customContextMenuRequested(); - void on_hMixerScrollArea_customContextMenuRequested(); - - void on_actionCopySource_triggered(); - void on_actionPasteRef_triggered(); - void on_actionPasteDup_triggered(); - - void on_actionCopyFilters_triggered(); - void on_actionPasteFilters_triggered(); - void AudioMixerCopyFilters(); - void AudioMixerPasteFilters(); - void SourcePasteFilters(OBSSource source, OBSSource dstSource); - - void on_previewXScrollBar_valueChanged(int value); - void on_previewYScrollBar_valueChanged(int value); - - void PreviewScalingModeChanged(int value); - - void ColorChange(); - - SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); - - void on_actionShowAbout_triggered(); - - void EnablePreview(); - void DisablePreview(); - - void EnablePreviewProgram(); - void DisablePreviewProgram(); - - void SceneCopyFilters(); - void ScenePasteFilters(); - - void CheckDiskSpaceRemaining(); - void OpenSavedProjector(SavedProjectorInfo *info); - - void ResetStatsHotkey(); - - void SetImageIcon(const QIcon &icon); - void SetColorIcon(const QIcon &icon); - void SetSlideshowIcon(const QIcon &icon); - void SetAudioInputIcon(const QIcon &icon); - void SetAudioOutputIcon(const QIcon &icon); - void SetDesktopCapIcon(const QIcon &icon); - void SetWindowCapIcon(const QIcon &icon); - void SetGameCapIcon(const QIcon &icon); - void SetCameraIcon(const QIcon &icon); - void SetTextIcon(const QIcon &icon); - void SetMediaIcon(const QIcon &icon); - void SetBrowserIcon(const QIcon &icon); - void SetGroupIcon(const QIcon &icon); - void SetSceneIcon(const QIcon &icon); - void SetDefaultIcon(const QIcon &icon); - void SetAudioProcessOutputIcon(const QIcon &icon); - - void TBarChanged(int value); - void TBarReleased(); - - void LockVolumeControl(bool lock); - - void UpdateVirtualCamConfig(const VCamConfig &config); - void RestartVirtualCam(const VCamConfig &config); - void RestartingVirtualCam(); - -private: - /* OBS Callbacks */ - static void SceneReordered(void *data, calldata_t *params); - static void SceneRefreshed(void *data, calldata_t *params); - static void SceneItemAdded(void *data, calldata_t *params); - static void SourceCreated(void *data, calldata_t *params); - static void SourceRemoved(void *data, calldata_t *params); - static void SourceActivated(void *data, calldata_t *params); - static void SourceDeactivated(void *data, calldata_t *params); - static void SourceAudioActivated(void *data, calldata_t *params); - static void SourceAudioDeactivated(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void RenderMain(void *data, uint32_t cx, uint32_t cy); - - void ResizePreview(uint32_t cx, uint32_t cy); - - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - void copyActionsDynamicProperties(); - - static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - - void AutoRemux(QString input, bool no_show = false); - - void UpdateIsRecordingPausable(); - - bool IsFFmpegOutputToURL() const; - bool OutputPathValid(); - void OutputPathInvalidMessage(); - - bool LowDiskSpace(); - void DiskSpaceMessage(); - - OBSSource prevFTBSource = nullptr; - - float dpi = 1.0; - -public: - OBSSource GetProgramSource(); - OBSScene GetCurrentScene(); - - void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); - - inline OBSSource GetCurrentSceneSource() - { - OBSScene curScene = GetCurrentScene(); - return OBSSource(obs_scene_get_source(curScene)); - } - - obs_service_t *GetService(); - void SetService(obs_service_t *service); - - int GetTransitionDuration(); - int GetTbarPosition(); - - inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } - - inline bool VCamEnabled() const { return vcamEnabled; } - - bool Active() const; - - void ResetUI(); - int ResetVideo(); - bool ResetAudio(); - - void ResetOutputs(); - - void RefreshVolumeColors(); - - void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - - void NewProject(); - void LoadProject(); - - inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) - { - x = previewX; - y = previewY; - cx = previewCX; - cy = previewCY; - } - - inline bool SavingDisabled() const { return disableSaving; } - - inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } - - void SaveService(); - bool LoadService(); - - inline Auth *GetAuth() { return auth.get(); } - - inline void EnableOutputs(bool enable) - { - if (enable) { - if (--disableOutputsRef < 0) - disableOutputsRef = 0; - } else { - disableOutputsRef++; - } - } - - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item); - void CreateSourcePopupMenu(int idx, bool preview); - - void UpdateTitleBar(); - - void SystemTrayInit(); - void SystemTray(bool firstStarted); - - void OpenSavedProjectors(); - - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CreateEditTransformWindow(obs_sceneitem_t *item); - - QAction *AddDockWidget(QDockWidget *dock); - void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); - void RemoveDockWidget(const QString &name); - bool IsDockObjectNameUsed(const QString &name); - void AddCustomDockWidget(QDockWidget *dock); - - static OBSBasic *Get(); - - const char *GetCurrentOutputPath(); - - void DeleteProjector(OBSProjector *projector); - - static QList GetProjectorMenuMonitorsFormatted(); - template - static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) - { - auto projectors = GetProjectorMenuMonitorsFormatted(); - for (int i = 0; i < projectors.size(); i++) { - QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); - action->setProperty("monitor", i); - } - } - - QIcon GetSourceIcon(const char *id) const; - QIcon GetGroupIcon() const; - QIcon GetSceneIcon() const; - - OBSWeakSource copyFilter; - - void ShowStatusBarMessage(const QString &message); - - static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); - void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); - - static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) - { - obs_scene_t *scene = obs_scene_from_source(scene_source); - return BackupScene(scene, sources); - } - - void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array); - - void SetDisplayAffinity(QWindow *window); - - QColor GetSelectionColor() const; - inline bool Closing() { return closing; } - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; - virtual void changeEvent(QEvent *event) override; - -private slots: - void on_actionFullscreenInterface_triggered(); - - void on_actionShow_Recordings_triggered(); - void on_actionRemux_triggered(); - void on_action_Settings_triggered(); - void on_actionShowMacPermissions_triggered(); - void on_actionShowMissingFiles_triggered(); - void on_actionAdvAudioProperties_triggered(); - void on_actionMixerToolbarAdvAudio_triggered(); - void on_actionMixerToolbarMenu_triggered(); - void on_actionShowLogs_triggered(); - void on_actionUploadCurrentLog_triggered(); - void on_actionUploadLastLog_triggered(); - void on_actionViewCurrentLog_triggered(); - void on_actionCheckForUpdates_triggered(); - void on_actionRepair_triggered(); - void on_actionShowWhatsNew_triggered(); - void on_actionRestartSafe_triggered(); - - void on_actionShowCrashLogs_triggered(); - void on_actionUploadLastCrashLog_triggered(); - - void on_actionEditTransform_triggered(); - void on_actionCopyTransform_triggered(); - void on_actionPasteTransform_triggered(); - void on_actionRotate90CW_triggered(); - void on_actionRotate90CCW_triggered(); - void on_actionRotate180_triggered(); - void on_actionFlipHorizontal_triggered(); - void on_actionFlipVertical_triggered(); - void on_actionFitToScreen_triggered(); - void on_actionStretchToScreen_triggered(); - void on_actionCenterToScreen_triggered(); - void on_actionVerticalCenter_triggered(); - void on_actionHorizontalCenter_triggered(); - void on_actionSceneFilters_triggered(); - - void on_OBSBasic_customContextMenuRequested(const QPoint &pos); - - void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); - void on_scenes_customContextMenuRequested(const QPoint &pos); - void GridActionClicked(); - void on_actionSceneListMode_triggered(); - void on_actionSceneGridMode_triggered(); - void on_actionAddScene_triggered(); - void on_actionRemoveScene_triggered(); - void on_actionSceneUp_triggered(); - void on_actionSceneDown_triggered(); - void on_sources_customContextMenuRequested(const QPoint &pos); - void on_scenes_itemDoubleClicked(QListWidgetItem *item); - void on_actionAddSource_triggered(); - void on_actionRemoveSource_triggered(); - void on_actionInteract_triggered(); - void on_actionSourceProperties_triggered(); - void on_actionSourceUp_triggered(); - void on_actionSourceDown_triggered(); - - void on_actionMoveUp_triggered(); - void on_actionMoveDown_triggered(); - void on_actionMoveToTop_triggered(); - void on_actionMoveToBottom_triggered(); - - void on_actionLockPreview_triggered(); - - void on_scalingMenu_aboutToShow(); - void on_actionScaleWindow_triggered(); - void on_actionScaleCanvas_triggered(); - void on_actionScaleOutput_triggered(); - - void Screenshot(OBSSource source_ = nullptr); - void ScreenshotSelectedSource(); - void ScreenshotProgram(); - void ScreenshotScene(); - - void on_actionHelpPortal_triggered(); - void on_actionWebsite_triggered(); - void on_actionDiscord_triggered(); - void on_actionReleaseNotes_triggered(); - - void on_preview_customContextMenuRequested(); - void ProgramViewContextMenuRequested(); - void on_previewDisabledWidget_customContextMenuRequested(); - - void on_actionShowSettingsFolder_triggered(); - void on_actionShowProfileFolder_triggered(); - - void on_actionAlwaysOnTop_triggered(); - - void on_toggleListboxToolbars_toggled(bool visible); - void on_toggleContextBar_toggled(bool visible); - void on_toggleStatusBar_toggled(bool visible); - void on_toggleSourceIcons_toggled(bool visible); - - void on_transitions_currentIndexChanged(int index); - void on_transitionAdd_clicked(); - void on_transitionRemove_clicked(); - void on_transitionProps_clicked(); - void on_transitionDuration_valueChanged(); - - void ShowTransitionProperties(); - void HideTransitionProperties(); - - // Source Context Buttons - void on_sourcePropertiesButton_clicked(); - void on_sourceFiltersButton_clicked(); - void on_sourceInteractButton_clicked(); - - void on_autoConfigure_triggered(); - void on_stats_triggered(); - - void on_resetUI_triggered(); - void on_resetDocks_triggered(bool force = false); - void on_lockDocks_toggled(bool lock); - void on_multiviewProjectorWindowed_triggered(); - void on_sideDocks_toggled(bool side); - - void logUploadFinished(const QString &text, const QString &error); - void crashUploadFinished(const QString &text, const QString &error); - void openLogDialog(const QString &text, const bool crash); - - void updateCheckFinished(); - - void MoveSceneToTop(); - void MoveSceneToBottom(); - - void EditSceneName(); - void EditSceneItemName(); - - void SceneNameEdited(QWidget *editor); - - void OpenSceneFilters(); - void OpenFilters(OBSSource source = nullptr); - void OpenProperties(OBSSource source = nullptr); - void OpenInteraction(OBSSource source = nullptr); - void OpenEditTransform(OBSSceneItem item = nullptr); - - void EnablePreviewDisplay(bool enable); - void TogglePreview(); - - void OpenStudioProgramProjector(); - void OpenPreviewProjector(); - void OpenSourceProjector(); - void OpenMultiviewProjector(); - void OpenSceneProjector(); - - void OpenStudioProgramWindow(); - void OpenPreviewWindow(); - void OpenSourceWindow(); - void OpenSceneWindow(); - - void StackedMixerAreaContextMenuRequested(); - - void ResizeOutputSizeOfSource(); - - void RepairOldExtraDockName(); - void RepairCustomExtraDockName(); - - /* Stream action (start/stop) slot */ - void StreamActionTriggered(); - - /* Record action (start/stop) slot */ - void RecordActionTriggered(); - - /* Record pause (pause/unpause) slot */ - void RecordPauseToggled(); - - /* Replay Buffer action (start/stop) slot */ - void ReplayBufferActionTriggered(); - - /* Virtual Cam action (start/stop) slots */ - void VirtualCamActionTriggered(); - - void OpenVirtualCamConfig(); - - /* Studio Mode toggle slot */ - void TogglePreviewProgramMode(); - -public slots: - void on_actionResetTransform_triggered(); - - bool StreamingActive(); - bool RecordingActive(); - bool ReplayBufferActive(); - bool VirtualCamActive(); - - void ClearContextBar(); - void UpdateContextBar(bool force = false); - void UpdateContextBarDeferred(bool force = false); - void UpdateContextBarVisibility(); - -signals: - /* Streaming signals */ - void StreamingPreparing(); - void StreamingStarting(bool broadcastAutoStart); - void StreamingStarted(bool withDelay = false); - void StreamingStopping(); - void StreamingStopped(bool withDelay = false); - - /* Broadcast Flow signals */ - void BroadcastFlowEnabled(bool enabled); - void BroadcastStreamReady(bool ready); - void BroadcastStreamActive(); - void BroadcastStreamStarted(bool autoStop); - - /* Recording signals */ - void RecordingStarted(bool pausable = false); - void RecordingPaused(); - void RecordingUnpaused(); - void RecordingStopping(); - void RecordingStopped(); - - /* Replay Buffer signals */ - void ReplayBufEnabled(bool enabled); - void ReplayBufStarted(); - void ReplayBufStopping(); - void ReplayBufStopped(); - - /* Virtual Camera signals */ - void VirtualCamEnabled(); - void VirtualCamStarted(); - void VirtualCamStopped(); - - /* Studio Mode signal */ - void PreviewProgramModeChanged(bool enabled); - void CanvasResized(uint32_t width, uint32_t height); - void OutputResized(uint32_t width, uint32_t height); - - /* Preview signals */ - void PreviewXScrollBarMoved(int value); - void PreviewYScrollBarMoved(int value); - -private: - std::unique_ptr ui; - - QPointer controlsDock; - -public: - /* `undo_s` needs to be declared after `ui` to prevent an uninitialized - * warning for `ui` while initializing `undo_s`. */ - undo_stack undo_s; - - explicit OBSBasic(QWidget *parent = 0); - virtual ~OBSBasic(); - - virtual void OBSInit() override; - - virtual config_t *Config() const override; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const override; - - static void InitBrowserPanelSafeBlock(); -#ifdef YOUTUBE_ENABLED - void NewYouTubeAppDock(); - void DeleteYouTubeAppDock(); - YouTubeAppDock *GetYouTubeAppDock(); -#endif - // MARK: - Generic UI Helper Functions - OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - - // MARK: - OBS Profile Management -private: - OBSProfileCache profiles{}; - - void SetupNewProfile(const std::string &profileName, bool useWizard = false); - void SetupDuplicateProfile(const std::string &profileName); - void SetupRenameProfile(const std::string &profileName); - - const OBSProfile &CreateProfile(const std::string &profileName); - void RemoveProfile(OBSProfile profile); - - void ChangeProfile(); - - void RefreshProfileCache(); - - void RefreshProfiles(bool refreshCache = false); - - void ActivateProfile(const OBSProfile &profile, bool reset = false); - void UpdateProfileEncoders(); - std::vector GetRestartRequirements(const ConfigFile &config) const; - void ResetProfileData(); - void CheckForSimpleModeX264Fallback(); - -public: - inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; - - const OBSProfile &GetCurrentProfile() const; - - std::optional GetProfileByName(const std::string &profileName) const; - std::optional GetProfileByDirectoryName(const std::string &directoryName) const; - -private slots: - void on_actionNewProfile_triggered(); - void on_actionDupProfile_triggered(); - void on_actionRenameProfile_triggered(); - void on_actionRemoveProfile_triggered(bool skipConfirmation = false); - void on_actionImportProfile_triggered(); - void on_actionExportProfile_triggered(); - -public slots: - bool CreateNewProfile(const QString &name); - bool CreateDuplicateProfile(const QString &name); - void DeleteProfile(const QString &profileName); - - // MARK: - OBS Scene Collection Management -private: - OBSSceneCollectionCache collections{}; - - void SetupNewSceneCollection(const std::string &collectionName); - void SetupDuplicateSceneCollection(const std::string &collectionName); - void SetupRenameSceneCollection(const std::string &collectionName); - - const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); - void RemoveSceneCollection(OBSSceneCollection collection); - - bool CreateDuplicateSceneCollection(const QString &name); - void DeleteSceneCollection(const QString &name); - void ChangeSceneCollection(); - - void RefreshSceneCollectionCache(); - - void RefreshSceneCollections(bool refreshCache = false); - void ActivateSceneCollection(const OBSSceneCollection &collection); - -public: - inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; - - const OBSSceneCollection &GetCurrentSceneCollection() const; - - std::optional GetSceneCollectionByName(const std::string &collectionName) const; - std::optional GetSceneCollectionByFileName(const std::string &fileName) const; - -private slots: - void on_actionNewSceneCollection_triggered(); - void on_actionDupSceneCollection_triggered(); - void on_actionRenameSceneCollection_triggered(); - void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); - void on_actionImportSceneCollection_triggered(); - void on_actionExportSceneCollection_triggered(); - void on_actionRemigrateSceneCollection_triggered(); - -public slots: - bool CreateNewSceneCollection(const QString &name); -}; - -extern bool cef_js_avail; - -class SceneRenameDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SceneRenameDelegate(QObject *parent); - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - -protected: - virtual bool eventFilter(QObject *editor, QEvent *event) override; -}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index 47cac8dcc..b840930ef 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -16,156 +16,91 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ + +#include "OBSBasic.hpp" #include "ui-config.h" +#include "ColorSelect.hpp" +#include "OBSBasicControls.hpp" +#include "OBSBasicStats.hpp" +#include "VolControl.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" #ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" +#include #endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include #endif +#include -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - +#include #ifdef BROWSER_AVAILABLE #include #endif +#ifdef ENABLE_WAYLAND +#include +#endif +#include -#include "ui-config.h" +#include +#include +#include -struct QCef; -struct QCefCookieManager; +#ifdef _WIN32 +#include +#endif +#include +#include -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include "Windows.h" +#endif +#include "moc_OBSBasic.cpp" + +using namespace std; + +extern bool portable_mode; +extern bool disable_3p_plugins; +extern bool opt_studio_mode; +extern bool opt_always_on_top; +extern bool opt_minimize_tray; extern std::string opt_starting_profile; extern std::string opt_starting_collection; -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace +extern bool safe_mode; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; extern volatile long insideEventLoop; +extern bool restart; -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); +extern bool EncoderAvailable(const char *encoder); -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} +struct QCef; -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} +extern QCef *cef; +extern bool cef_js_avail; -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} +extern void DestroyPanelCookieManager(); +extern void CheckExistingCookieId(); static void AddExtraModulePaths() { @@ -259,52 +194,7 @@ static void SetSafeModuleNames() #endif } -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif +extern void setupDockAction(QDockWidget *dock); OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) { @@ -604,980 +494,8 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new UpdatePreviewOverflowSettings(); } -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; -extern void CheckExistingCookieId(); - #ifdef __APPLE__ #define DEFAULT_CONTAINER "fragmented_mov" #elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 @@ -1858,8 +776,6 @@ bool OBSBasic::InitBasicConfigDefaults() return true; } -extern bool EncoderAvailable(const char *encoder); - void OBSBasic::InitBasicConfigDefaults2() { bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); @@ -1937,82 +853,6 @@ void OBSBasic::InitOBSCallbacks() this); } -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - #define STARTUP_SEPARATOR "==== Startup complete ===============================================" #define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" @@ -2461,398 +1301,6 @@ void OBSBasic::OnFirstLoad() on_actionViewCurrentLog_triggered(); } -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - OBSBasic::~OBSBasic() { /* clear out UI event queue */ @@ -2953,1430 +1401,6 @@ OBSBasic::~OBSBasic() #endif } -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - static inline int AttemptToResetVideo(struct obs_video_info *ovi) { return obs_reset_video(ovi); @@ -4439,18 +1463,6 @@ static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) return colorspace; } -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - int OBSBasic::ResetVideo() { if (outputHandler && outputHandler->Active()) @@ -4556,220 +1568,6 @@ bool OBSBasic::ResetAudio() return obs_reset_audio2(&ai); } -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - void OBSBasic::closeEvent(QCloseEvent *event) { /* Wait for multitrack video stream to start/finish processing in the background */ @@ -4935,2783 +1733,6 @@ void OBSBasic::changeEvent(QEvent *event) } } -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const { const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); @@ -7788,49 +1809,6 @@ config_t *OBSBasic::Config() const return activeConfiguration; } -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - void OBSBasic::UpdateEditMenu() { QModelIndexList items = GetAllSelectedSourceItems(); @@ -7901,748 +1879,6 @@ void OBSBasic::UpdateEditMenu() ui->actionHorizontalCenter->setEnabled(canTransformMultiple); } -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - void OBSBasic::UpdateTitleBar() { stringstream name; @@ -8666,1095 +1902,11 @@ void OBSBasic::UpdateTitleBar() setWindowTitle(QT_UTF8(name.str().c_str())); } -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - OBSBasic *OBSBasic::Get() { return reinterpret_cast(App()->GetMainWindow()); } -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) { if (!error.isEmpty()) @@ -9763,292 +1915,6 @@ void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) patronJson = QT_TO_UTF8(text); } -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - void OBSBasic::SetDisplayAffinity(QWindow *window) { if (!SetDisplayAffinitySupported()) @@ -10078,89 +1944,12 @@ void OBSBasic::SetDisplayAffinity(QWindow *window) #endif } -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - void OBSBasic::OnEvent(enum obs_frontend_event event) { if (api) api->on_event(event); } -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) { OBSPromptResult result; diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 81bb7b478..e8169c50e 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -17,49 +17,51 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "window-main.hpp" -#include "window-basic-interaction.hpp" -#include "window-basic-vcam.hpp" -#include "window-basic-properties.hpp" -#include "window-basic-transform.hpp" -#include "window-basic-adv-audio.hpp" -#include "window-basic-filters.hpp" -#include "window-missing-files.hpp" -#include "window-projector.hpp" -#include "window-basic-about.hpp" -#ifdef YOUTUBE_ENABLED -#include "window-dock-youtube-app.hpp" -#endif -#include "auth-base.hpp" -#include "log-viewer.hpp" -#include "undo-stack-obs.hpp" +#include "ui_OBSBasic.h" +#include "OBSMainWindow.hpp" + +#include +#include +#include +#include +#include +#include #include +#include +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); + +#include #include #include #include -#include +#include -class QMessageBox; -class QListWidgetItem; +#include + +extern volatile bool recording_paused; + +class ColorSelect; +class OBSAbout; +class OBSBasicAdvAudio; +class OBSBasicFilters; +class OBSBasicInteraction; +class OBSBasicProperties; +class OBSBasicTransform; +class OBSLogViewer; +class OBSMissingFiles; +class OBSProjector; class VolControl; -class OBSBasicStats; -class OBSBasicVCamConfig; - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" +#ifdef YOUTUBE_ENABLED +class YouTubeAppDock; +#endif +class QMessageBox; +class QWidgetAction; +struct QuickTransition; #define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") #define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") @@ -83,7 +85,7 @@ class OBSBasicVCamConfig; #define PREVIEW_EDGE_SIZE 10 -struct BasicOutputHandler; +enum class ProjectorType; enum class QtDataRole { OBSRef = Qt::UserRole, @@ -108,30 +110,6 @@ struct SourceCopyInfo { obs_blending_type blend_mode; }; -struct QuickTransition { - QPushButton *button = nullptr; - OBSSource source; - obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; - int duration = 0; - int id = 0; - bool fadeToBlack = false; - - inline QuickTransition() {} - inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) - : source(source_), - duration(duration_), - id(id_), - fadeToBlack(fadeToBlack_), - renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", - SourceRenamed, this)) - { - } - -private: - static void SourceRenamed(void *param, calldata_t *data); - std::shared_ptr renamedSignal; -}; - struct OBSProfile { std::string name; std::string directoryName; @@ -165,14 +143,60 @@ using OBSPromptCallback = std::function; using OBSProfileCache = std::map; using OBSSceneCollectionCache = std::map; -class ColorSelect : public QWidget { +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} -public: - explicit ColorSelect(QWidget *parent = 0); +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} -private: - std::unique_ptr ui; -}; +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif class OBSBasic : public OBSMainWindow { Q_OBJECT @@ -198,19 +222,12 @@ class OBSBasic : public OBSMainWindow { friend class OBSBasicPreview; friend class OBSBasicStatusBar; friend class OBSBasicSourceSelect; - friend class OBSBasicTransform; friend class OBSBasicSettings; friend class Auth; friend class AutoConfig; friend class AutoConfigStreamPage; - friend class RecordButton; - friend class ControlsSplitButton; friend class ExtraBrowsersModel; - friend class ExtraBrowsersDelegate; - friend class DeviceCaptureToolbar; - friend class OBSBasicSourceSelect; friend class OBSYoutubeActions; - friend class OBSPermissions; friend struct BasicOutputHandler; friend struct OBSStudioAPI; friend class ScreenshotObj; @@ -233,337 +250,102 @@ class OBSBasic : public OBSMainWindow { Vertical, Horizontal, }; - + /* ------------------------------------- + * MARK: - General + * ------------------------------------- + */ private: obs_frontend_callbacks *api = nullptr; - - std::shared_ptr auth; - - std::vector volumes; - std::vector signalHandlers; - QList> oldExtraDocks; - QStringList oldExtraDockNames; - - OBSDataAutoRelease collectionModuleData; - std::vector safeModeTransitions; - bool loaded = false; - long disableSaving = 1; - bool projectChanged = false; - bool previewEnabled = true; - ContextBarSize contextBarSize = ContextBarSize_Normal; - - std::deque clipboard; - OBSWeakSourceAutoRelease copyFiltersSource; - bool copyVisible = true; - obs_transform_info copiedTransformInfo; - obs_sceneitem_crop copiedCropInfo; - bool hasCopiedTransform = false; - OBSWeakSourceAutoRelease copySourceTransition; - int copySourceTransitionDuration; - bool closing = false; - bool clearingFailed = false; + // TODO: Remove, orphaned variable + bool copyVisible = true; + // TODO: Unused thread pointer, remove. QScopedPointer devicePropertiesThread; - QScopedPointer whatsNewInitThread; - QScopedPointer updateCheckThread; - QScopedPointer introCheckThread; + QScopedPointer logUploadThread; - QPointer interaction; - QPointer properties; - QPointer transformWindow; - QPointer advAudioWindow; - QPointer filters; - QPointer statsDock; -#ifdef YOUTUBE_ENABLED - QPointer youtubeAppDock; - uint64_t lastYouTubeAppDockCreationTime = 0; -#endif - QPointer about; - QPointer missDialog; - QPointer logView; - - QPointer cpuUsageTimer; - QPointer diskFullTimer; - - QPointer nudge_timer; - bool recent_nudge = false; - - os_cpu_usage_info_t *cpuUsageInfo = nullptr; - - OBSService service; - std::unique_ptr outputHandler; - std::shared_future setupStreamingGuard; - bool streamingStopping = false; - bool recordingStopping = false; - bool replayBufferStopping = false; - - gs_vertbuffer_t *box = nullptr; - gs_vertbuffer_t *boxLeft = nullptr; - gs_vertbuffer_t *boxTop = nullptr; - gs_vertbuffer_t *boxRight = nullptr; - gs_vertbuffer_t *boxBottom = nullptr; - gs_vertbuffer_t *circle = nullptr; - - gs_vertbuffer_t *actionSafeMargin = nullptr; - gs_vertbuffer_t *graphicsSafeMargin = nullptr; - gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; - - int previewX = 0, previewY = 0; - int previewCX = 0, previewCY = 0; - float previewScale = 0.0f; - ConfigFile activeConfiguration; - std::vector savedProjectorsArray; - std::vector projectors; - - QPointer stats; - QPointer remux; - QPointer extraBrowsers; - QPointer importer; - - QPointer transitionButton; - - bool vcamEnabled = false; - VCamConfig vcamConfig; - - QScopedPointer trayIcon; - QPointer sysTrayStream; - QPointer sysTrayRecord; - QPointer sysTrayReplayBuffer; - QPointer sysTrayVirtualCam; - QPointer showHide; - QPointer exit; - QPointer trayMenu; - QPointer previewProjector; - QPointer studioProgramProjector; - QPointer previewProjectorSource; - QPointer previewProjectorMain; - QPointer sceneProjectorMenu; - QPointer sourceProjector; - QPointer scaleFilteringMenu; - QPointer blendingMethodMenu; - QPointer blendingModeMenu; - QPointer colorMenu; - QPointer colorWidgetAction; - QPointer colorSelect; - QPointer deinterlaceMenu; - QPointer perSceneTransitionMenu; - QPointer shortcutFilter; - QPointer renameScene; - QPointer renameSource; - - QPointer programWidget; - QPointer programLayout; - QPointer programLabel; - QScopedPointer patronJsonThread; std::string patronJson; - std::atomic currentScene = nullptr; - std::optional> lastOutputResolution; - std::optional> migrationBaseResolution; - bool usingAbsoluteCoordinates = false; - - void DisableRelativeCoordinates(bool disable); + std::unique_ptr ui; void OnEvent(enum obs_frontend_event event); - void UpdateMultiviewProjectorMenu(); - - void DrawBackdrop(float cx, float cy); - - void SetupEncoders(); - - void CreateFirstRunSources(); - void CreateDefaultScene(bool firstStart); - - void UpdateVolumeControlsDecayRate(); - void UpdateVolumeControlsPeakMeterType(); - void ClearVolumeControls(); - - void UploadLog(const char *subdir, const char *file, const bool crash); - - void Save(const char *file); - void LoadData(obs_data_t *data, const char *file, bool remigrate = false); - void Load(const char *file, bool remigrate = false); - - void InitHotkeys(); - void CreateHotkeys(); - void ClearHotkeys(); - - bool InitService(); - bool InitBasicConfigDefaults(); void InitBasicConfigDefaults2(); bool InitBasicConfig(); void InitOBSCallbacks(); - void InitPrimitives(); - void OnFirstLoad(); - OBSSceneItem GetSceneItem(QListWidgetItem *item); - OBSSceneItem GetCurrentSceneItem(); - - bool QueryRemoveSource(obs_source_t *source); - - void TimedCheckForUpdates(); - void CheckForUpdates(bool manualUpdate); - void GetFPSCommon(uint32_t &num, uint32_t &den) const; void GetFPSInteger(uint32_t &num, uint32_t &den) const; void GetFPSFraction(uint32_t &num, uint32_t &den) const; void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; void GetConfigFPS(uint32_t &num, uint32_t &den) const; - void UpdatePreviewScalingMenu(); + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - void LoadSceneListOrder(obs_data_array_t *array); - obs_data_array_t *SaveSceneListOrder(); - void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + // TODO: Remove, orphaned instance method + void NewProject(); + // TODO: Remove, orphaned instance method + void LoadProject(); - void TempFileOutput(const char *path, int vBitrate, int aBitrate); - void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); +public slots: + void UpdatePatronJson(const QString &text, const QString &error); + void UpdateEditMenu(); - void CloseDialogs(); - void ClearSceneData(); - void ClearProjectors(); +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; - void Nudge(int dist, MoveDir dir); + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); - OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + virtual void OBSInit() override; - void GetAudioSourceFilters(); - void GetAudioSourceProperties(); - void VolControlContextMenu(); - void ToggleVolControlLayout(); - void ToggleMixerLayout(bool vertical); + virtual config_t *Config() const override; - void LogScenes(); - void SaveProjectNow(); + int ResetVideo(); + bool ResetAudio(); - int GetTopSelectedSourceItem(); + void UpdateTitleBar(); - QModelIndexList GetAllSelectedSourceItems(); + static OBSBasic *Get(); - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, - togglePreviewHotkeys, contextBarHotkeys; - obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + void SetDisplayAffinity(QWindow *window); - void InitDefaultTransitions(); - void InitTransition(obs_source_t *transition); - obs_source_t *FindTransition(const char *name); - OBSSource GetCurrentTransition(); - obs_data_array_t *SaveTransitions(); - void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + inline bool Closing() { return closing; } - obs_source_t *fadeTransition; - obs_source_t *cutTransition; +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; - void CreateProgramDisplay(); - void CreateProgramOptions(); - void AddQuickTransitionId(int id); - void AddQuickTransition(); - void AddQuickTransitionHotkey(QuickTransition *qt); - void RemoveQuickTransitionHotkey(QuickTransition *qt); - void LoadQuickTransitions(obs_data_array_t *array); - obs_data_array_t *SaveQuickTransitions(); - void ClearQuickTransitionWidgets(); - void RefreshQuickTransitions(); - void DisableQuickTransitionWidgets(); - void EnableTransitionWidgets(bool enable); - void CreateDefaultQuickTransitions(); + /* ------------------------------------- + * MARK: - OAuth + * ------------------------------------- + */ +private: + std::shared_ptr auth; - void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); - QMenu *CreatePerSceneTransitionMenu(); - QMenu *CreateVisibilityTransitionMenu(bool visible); +public: + inline Auth *GetAuth() { return auth.get(); } - QuickTransition *GetQuickTransition(int id); - int GetQuickTransitionIdx(int id); - QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); - void ClearQuickTransitions(); - void QuickTransitionClicked(); - void QuickTransitionChange(); - void QuickTransitionChangeDuration(int value); - void QuickTransitionRemoveClicked(); - - void SetPreviewProgramMode(bool enabled); - void ResizeProgram(uint32_t cx, uint32_t cy); - void SetCurrentScene(obs_scene_t *scene, bool force = false); - static void RenderProgram(void *data, uint32_t cx, uint32_t cy); - - std::vector quickTransitions; - QPointer programOptions; - QPointer program; - OBSWeakSource lastScene; - OBSWeakSource swapScene; - OBSWeakSource programScene; - OBSWeakSource lastProgramScene; - bool editPropertiesMode = false; - bool sceneDuplicationMode = true; - bool swapScenesMode = true; - volatile bool previewProgramMode = false; - obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; - obs_hotkey_id transitionHotkey = 0; - obs_hotkey_id statsHotkey = 0; - obs_hotkey_id screenshotHotkey = 0; - obs_hotkey_id sourceScreenshotHotkey = 0; - int quickTransitionIdCounter = 1; - bool overridingTransition = false; - - int programX = 0, programY = 0; - int programCX = 0, programCY = 0; - float programScale = 0.0f; - - int disableOutputsRef = 0; - - inline void OnActivate(bool force = false); - inline void OnDeactivate(); - - void AddDropSource(const char *file, DropType image); - void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - - bool sysTrayMinimizeToTray(); - - void EnumDialogs(); - - QList visDialogs; - QList modalDialogs; - QList visMsgBoxes; - - QList visDlgPositions; - - QByteArray startingDockLayout; - - obs_data_array_t *SaveProjectors(); - void LoadSavedProjectors(obs_data_array_t *savedProjectors); - - void MacBranchesFetched(const QString &branch, bool manualUpdate); - void ReceivedIntroJson(const QString &text); - void ShowWhatsNew(const QString &url); - - void UpdatePreviewProgramIndicators(); - - QStringList extraDockNames; - QList> extraDocks; - - QStringList extraCustomDockNames; - QList> extraCustomDocks; + /* ------------------------------------- + * MARK: - OBSBasic_Browser + * ------------------------------------- + */ +private: + QPointer extraBrowsers; #ifdef BROWSER_AVAILABLE QPointer extraBrowserMenuDocksSeparator; @@ -579,6 +361,141 @@ private: void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); #endif +public: + static void InitBrowserPanelSafeBlock(); + + /* ------------------------------------- + * MARK: - OBSBasic_Clipboard + * ------------------------------------- + */ +private: + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + int copySourceTransitionDuration; + OBSWeakSourceAutoRelease copySourceTransition; + +private slots: + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + +public: + OBSWeakSource copyFilter; + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + /* ------------------------------------- + * MARK: - OBSBasic_ContextToolbar + * ------------------------------------- + */ +private: + ContextBarSize contextBarSize = ContextBarSize_Normal; + + void SourceToolBarActionsSetEnabled(); + + void copyActionsDynamicProperties(); + +private slots: + void on_toggleContextBar_toggled(bool visible); + +public slots: + void ShowContextBar(); + void HideContextBar(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + + /* ------------------------------------- + * MARK: - OBSBasic_Docks + * ------------------------------------- + */ +private: + QList> oldExtraDocks; + QStringList oldExtraDockNames; + QPointer statsDock; + QByteArray startingDockLayout; + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + + QPointer controlsDock; + +public: + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + +private slots: + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_sideDocks_toggled(bool side); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* ------------------------------------- + * MARK: - OBSBasic_Dropfiles + * ------------------------------------- + */ +private: + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + /* ------------------------------------- + * MARK: - OBSBasic_Hotkeys + * ------------------------------------- + */ +private: + QPointer shortcutFilter; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + +private slots: + void ProcessHotkey(obs_hotkey_id id, bool pressed); + void ResetStatsHotkey(); + + /* ------------------------------------- + * MARK: - OBSBasic_Icons + * ------------------------------------- + */ +private: QIcon imageIcon; QIcon colorIcon; QIcon slideshowIcon; @@ -611,216 +528,7 @@ private: QIcon GetDefaultIcon() const; QIcon GetAudioProcessOutputIcon() const; - QSlider *tBar; - bool tBarActive = false; - - OBSSource GetOverrideTransition(OBSSource source); - int GetOverrideTransitionDuration(OBSSource source); - - void UpdateProjectorHideCursor(); - void UpdateProjectorAlwaysOnTop(bool top); - void ResetProjectors(); - - QPointer screenshotData; - - void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); - - void UpdatePreviewSafeAreas(); - bool drawSafeAreas = false; - - void CenterSelectedSceneItems(const CenterType ¢erType); - void ShowMissingFilesDialog(obs_missing_files_t *files); - - QColor selectionColor; - QColor cropColor; - QColor hoverColor; - - QColor GetCropColor() const; - QColor GetHoverColor() const; - - void UpdatePreviewSpacingHelpers(); - bool drawSpacingHelpers = true; - - float GetDevicePixelRatio(); - void SourceToolBarActionsSetEnabled(); - - std::string lastScreenshot; - std::string lastReplay; - - void UpdatePreviewOverflowSettings(); - void UpdatePreviewScrollbars(); - - bool streamingStarting = false; - - bool recordingStarted = false; - bool isRecordingPausable = false; - bool recordingPaused = false; - - bool restartingVCam = false; - -public slots: - void DeferSaveBegin(); - void DeferSaveEnd(); - - void DisplayStreamStartError(); - - void SetupBroadcast(); - - void StartStreaming(); - void StopStreaming(); - void ForceStopStreaming(); - - void StreamDelayStarting(int sec); - void StreamDelayStopping(int sec); - - void StreamingStart(); - void StreamStopping(); - void StreamingStop(int errorcode, QString last_error); - - void StartRecording(); - void StopRecording(); - - void RecordingStart(); - void RecordStopping(); - void RecordingStop(int code, QString last_error); - void RecordingFileChanged(QString lastRecordingPath); - - void ShowReplayBufferPauseWarning(); - void StartReplayBuffer(); - void StopReplayBuffer(); - - void ReplayBufferStart(); - void ReplayBufferSave(); - void ReplayBufferSaved(); - void ReplayBufferStopping(); - void ReplayBufferStop(int code); - - void StartVirtualCam(); - void StopVirtualCam(); - - void OnVirtualCamStart(); - void OnVirtualCamStop(int code); - - void SaveProjectDeferred(); - void SaveProject(); - - void SetTransition(OBSSource transition); - void OverrideTransition(OBSSource transition); - void TransitionToScene(OBSScene scene, bool force = false); - void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, - bool black = false, bool manual = false); - void SetCurrentScene(OBSSource scene, bool force = false); - - void UpdatePatronJson(const QString &text, const QString &error); - - void ShowContextBar(); - void HideContextBar(); - void PauseRecording(); - void UnpauseRecording(); - - void UpdateEditMenu(); - private slots: - - void on_actionMainUndo_triggered(); - void on_actionMainRedo_triggered(); - - void AddSceneItem(OBSSceneItem item); - void AddScene(OBSSource source); - void RemoveScene(OBSSource source); - void RenameSources(OBSSource source, QString newName, QString prevName); - - void ActivateAudioSource(OBSSource source); - void DeactivateAudioSource(OBSSource source); - - void DuplicateSelectedScene(); - void RemoveSelectedScene(); - - void ToggleAlwaysOnTop(); - - void ReorderSources(OBSScene scene); - void RefreshSources(OBSScene scene); - - void ProcessHotkey(obs_hotkey_id id, bool pressed); - - void AddTransition(const char *id); - void RenameTransition(OBSSource transition); - void TransitionClicked(); - void TransitionStopped(); - void TransitionFullyStopped(); - void TriggerQuickTransition(int id); - - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - - void IconActivated(QSystemTrayIcon::ActivationReason reason); - void SetShowing(bool showing); - - void ToggleShowHide(); - - void HideAudioControl(); - void UnhideAllAudioControls(); - void ToggleHideMixer(); - - void MixerRenameSource(); - - void on_vMixerScrollArea_customContextMenuRequested(); - void on_hMixerScrollArea_customContextMenuRequested(); - - void on_actionCopySource_triggered(); - void on_actionPasteRef_triggered(); - void on_actionPasteDup_triggered(); - - void on_actionCopyFilters_triggered(); - void on_actionPasteFilters_triggered(); - void AudioMixerCopyFilters(); - void AudioMixerPasteFilters(); - void SourcePasteFilters(OBSSource source, OBSSource dstSource); - - void on_previewXScrollBar_valueChanged(int value); - void on_previewYScrollBar_valueChanged(int value); - - void PreviewScalingModeChanged(int value); - - void ColorChange(); - - SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); - - void on_actionShowAbout_triggered(); - - void EnablePreview(); - void DisablePreview(); - - void EnablePreviewProgram(); - void DisablePreviewProgram(); - - void SceneCopyFilters(); - void ScenePasteFilters(); - - void CheckDiskSpaceRemaining(); - void OpenSavedProjector(SavedProjectorInfo *info); - - void ResetStatsHotkey(); - void SetImageIcon(const QIcon &icon); void SetColorIcon(const QIcon &icon); void SetSlideshowIcon(const QIcon &icon); @@ -838,202 +546,64 @@ private slots: void SetDefaultIcon(const QIcon &icon); void SetAudioProcessOutputIcon(const QIcon &icon); - void TBarChanged(int value); - void TBarReleased(); - - void LockVolumeControl(bool lock); - - void UpdateVirtualCamConfig(const VCamConfig &config); - void RestartVirtualCam(const VCamConfig &config); - void RestartingVirtualCam(); - -private: - /* OBS Callbacks */ - static void SceneReordered(void *data, calldata_t *params); - static void SceneRefreshed(void *data, calldata_t *params); - static void SceneItemAdded(void *data, calldata_t *params); - static void SourceCreated(void *data, calldata_t *params); - static void SourceRemoved(void *data, calldata_t *params); - static void SourceActivated(void *data, calldata_t *params); - static void SourceDeactivated(void *data, calldata_t *params); - static void SourceAudioActivated(void *data, calldata_t *params); - static void SourceAudioDeactivated(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void RenderMain(void *data, uint32_t cx, uint32_t cy); - - void ResizePreview(uint32_t cx, uint32_t cy); - - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - void copyActionsDynamicProperties(); - - static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - - void AutoRemux(QString input, bool no_show = false); - - void UpdateIsRecordingPausable(); - - bool IsFFmpegOutputToURL() const; - bool OutputPathValid(); - void OutputPathInvalidMessage(); - - bool LowDiskSpace(); - void DiskSpaceMessage(); - - OBSSource prevFTBSource = nullptr; - - float dpi = 1.0; - public: - OBSSource GetProgramSource(); - OBSScene GetCurrentScene(); - - void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); - - inline OBSSource GetCurrentSceneSource() - { - OBSScene curScene = GetCurrentScene(); - return OBSSource(obs_scene_get_source(curScene)); - } - - obs_service_t *GetService(); - void SetService(obs_service_t *service); - - int GetTransitionDuration(); - int GetTbarPosition(); - - inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } - - inline bool VCamEnabled() const { return vcamEnabled; } - - bool Active() const; - - void ResetUI(); - int ResetVideo(); - bool ResetAudio(); - - void ResetOutputs(); - - void RefreshVolumeColors(); - - void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - - void NewProject(); - void LoadProject(); - - inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) - { - x = previewX; - y = previewY; - cx = previewCX; - cy = previewCY; - } - - inline bool SavingDisabled() const { return disableSaving; } - - inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } - - void SaveService(); - bool LoadService(); - - inline Auth *GetAuth() { return auth.get(); } - - inline void EnableOutputs(bool enable) - { - if (enable) { - if (--disableOutputsRef < 0) - disableOutputsRef = 0; - } else { - disableOutputsRef++; - } - } - - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item); - void CreateSourcePopupMenu(int idx, bool preview); - - void UpdateTitleBar(); - - void SystemTrayInit(); - void SystemTray(bool firstStarted); - - void OpenSavedProjectors(); - - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CreateEditTransformWindow(obs_sceneitem_t *item); - - QAction *AddDockWidget(QDockWidget *dock); - void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); - void RemoveDockWidget(const QString &name); - bool IsDockObjectNameUsed(const QString &name); - void AddCustomDockWidget(QDockWidget *dock); - - static OBSBasic *Get(); - - const char *GetCurrentOutputPath(); - - void DeleteProjector(OBSProjector *projector); - - static QList GetProjectorMenuMonitorsFormatted(); - template - static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) - { - auto projectors = GetProjectorMenuMonitorsFormatted(); - for (int i = 0; i < projectors.size(); i++) { - QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); - action->setProperty("monitor", i); - } - } - QIcon GetSourceIcon(const char *id) const; QIcon GetGroupIcon() const; QIcon GetSceneIcon() const; - OBSWeakSource copyFilter; + /* ------------------------------------- + * MARK: - OBSBasic_MainControls + * ------------------------------------- + */ +private: + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer about; + QPointer logView; + QPointer stats; + QPointer remux; + QPointer importer; + QPointer showHide; + QPointer exit; - void ShowStatusBarMessage(const QString &message); + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer deinterlaceMenu; - static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); - void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + QPointer colorWidgetAction; + QPointer colorSelect; - static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) - { - obs_scene_t *scene = obs_scene_from_source(scene_source); - return BackupScene(scene, sources); - } + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; - void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array); + QList visDlgPositions; - void SetDisplayAffinity(QWindow *window); - - QColor GetSelectionColor() const; - inline bool Closing() { return closing; } - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; - virtual void changeEvent(QEvent *event) override; + void UploadLog(const char *subdir, const char *file, const bool crash); + void CloseDialogs(); + void EnumDialogs(); private slots: - void on_actionFullscreenInterface_triggered(); + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + void ToggleAlwaysOnTop(); - void on_actionShow_Recordings_triggered(); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void on_actionShowAbout_triggered(); + + void on_actionFullscreenInterface_triggered(); void on_actionRemux_triggered(); void on_action_Settings_triggered(); void on_actionShowMacPermissions_triggered(); - void on_actionShowMissingFiles_triggered(); void on_actionAdvAudioProperties_triggered(); - void on_actionMixerToolbarAdvAudio_triggered(); - void on_actionMixerToolbarMenu_triggered(); void on_actionShowLogs_triggered(); void on_actionUploadCurrentLog_triggered(); void on_actionUploadLastLog_triggered(); @@ -1046,45 +616,220 @@ private slots: void on_actionShowCrashLogs_triggered(); void on_actionUploadLastCrashLog_triggered(); - void on_actionEditTransform_triggered(); - void on_actionCopyTransform_triggered(); - void on_actionPasteTransform_triggered(); - void on_actionRotate90CW_triggered(); - void on_actionRotate90CCW_triggered(); - void on_actionRotate180_triggered(); - void on_actionFlipHorizontal_triggered(); - void on_actionFlipVertical_triggered(); - void on_actionFitToScreen_triggered(); - void on_actionStretchToScreen_triggered(); - void on_actionCenterToScreen_triggered(); - void on_actionVerticalCenter_triggered(); - void on_actionHorizontalCenter_triggered(); - void on_actionSceneFilters_triggered(); - void on_OBSBasic_customContextMenuRequested(const QPoint &pos); - void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); - void on_scenes_customContextMenuRequested(const QPoint &pos); - void GridActionClicked(); - void on_actionSceneListMode_triggered(); - void on_actionSceneGridMode_triggered(); - void on_actionAddScene_triggered(); - void on_actionRemoveScene_triggered(); - void on_actionSceneUp_triggered(); - void on_actionSceneDown_triggered(); - void on_sources_customContextMenuRequested(const QPoint &pos); - void on_scenes_itemDoubleClicked(QListWidgetItem *item); - void on_actionAddSource_triggered(); - void on_actionRemoveSource_triggered(); - void on_actionInteract_triggered(); - void on_actionSourceProperties_triggered(); - void on_actionSourceUp_triggered(); - void on_actionSourceDown_triggered(); + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); - void on_actionMoveUp_triggered(); - void on_actionMoveDown_triggered(); - void on_actionMoveToTop_triggered(); - void on_actionMoveToBottom_triggered(); + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + +public: + void ResetUI(); + + void CreateInteractionWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + void CreatePropertiesWindow(obs_source_t *source); + + /* ------------------------------------- + * MARK: - OBSBasic_OutputHandler + * ------------------------------------- + */ +private: + std::unique_ptr outputHandler; + std::optional> lastOutputResolution; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false) + { + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon( + QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } + } + + inline void OnDeactivate() + { + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } + } + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + // TODO: Unimplemented, remove. + void SetupEncoders(); + // TODO: Unimplemented, remove. + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + // TODO: Unimplemented, remove. + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + +public: + bool Active() const; + void ResetOutputs(); + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + const char *GetCurrentOutputPath(); + +private slots: + void ResizeOutputSizeOfSource(); + + /* ------------------------------------- + * MARK: - OBSBasic_Preview + * ------------------------------------- + */ +private: + bool previewEnabled = true; + QPointer nudge_timer; + bool recent_nudge = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + bool drawSafeAreas = false; + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + bool drawSpacingHelpers = true; + + float dpi = 1.0; + + void DrawBackdrop(float cx, float cy); + void InitPrimitives(); + void UpdatePreviewScalingMenu(); + + void Nudge(int dist, MoveDir dir); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + void UpdatePreviewSafeAreas(); + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + + float GetDevicePixelRatio(); + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + /* OBS Callbacks */ + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + +private slots: + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + void EnablePreview(); + void DisablePreview(); void on_actionLockPreview_triggered(); @@ -1093,161 +838,24 @@ private slots: void on_actionScaleCanvas_triggered(); void on_actionScaleOutput_triggered(); - void Screenshot(OBSSource source_ = nullptr); - void ScreenshotSelectedSource(); - void ScreenshotProgram(); - void ScreenshotScene(); - - void on_actionHelpPortal_triggered(); - void on_actionWebsite_triggered(); - void on_actionDiscord_triggered(); - void on_actionReleaseNotes_triggered(); - void on_preview_customContextMenuRequested(); - void ProgramViewContextMenuRequested(); void on_previewDisabledWidget_customContextMenuRequested(); - void on_actionShowSettingsFolder_triggered(); - void on_actionShowProfileFolder_triggered(); - - void on_actionAlwaysOnTop_triggered(); - - void on_toggleListboxToolbars_toggled(bool visible); - void on_toggleContextBar_toggled(bool visible); - void on_toggleStatusBar_toggled(bool visible); - void on_toggleSourceIcons_toggled(bool visible); - - void on_transitions_currentIndexChanged(int index); - void on_transitionAdd_clicked(); - void on_transitionRemove_clicked(); - void on_transitionProps_clicked(); - void on_transitionDuration_valueChanged(); - - void ShowTransitionProperties(); - void HideTransitionProperties(); - - // Source Context Buttons - void on_sourcePropertiesButton_clicked(); - void on_sourceFiltersButton_clicked(); - void on_sourceInteractButton_clicked(); - - void on_autoConfigure_triggered(); - void on_stats_triggered(); - - void on_resetUI_triggered(); - void on_resetDocks_triggered(bool force = false); - void on_lockDocks_toggled(bool lock); - void on_multiviewProjectorWindowed_triggered(); - void on_sideDocks_toggled(bool side); - - void logUploadFinished(const QString &text, const QString &error); - void crashUploadFinished(const QString &text, const QString &error); - void openLogDialog(const QString &text, const bool crash); - - void updateCheckFinished(); - - void MoveSceneToTop(); - void MoveSceneToBottom(); - - void EditSceneName(); - void EditSceneItemName(); - - void SceneNameEdited(QWidget *editor); - - void OpenSceneFilters(); - void OpenFilters(OBSSource source = nullptr); - void OpenProperties(OBSSource source = nullptr); - void OpenInteraction(OBSSource source = nullptr); - void OpenEditTransform(OBSSceneItem item = nullptr); - void EnablePreviewDisplay(bool enable); void TogglePreview(); - void OpenStudioProgramProjector(); - void OpenPreviewProjector(); - void OpenSourceProjector(); - void OpenMultiviewProjector(); - void OpenSceneProjector(); +public: + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } - void OpenStudioProgramWindow(); - void OpenPreviewWindow(); - void OpenSourceWindow(); - void OpenSceneWindow(); - - void StackedMixerAreaContextMenuRequested(); - - void ResizeOutputSizeOfSource(); - - void RepairOldExtraDockName(); - void RepairCustomExtraDockName(); - - /* Stream action (start/stop) slot */ - void StreamActionTriggered(); - - /* Record action (start/stop) slot */ - void RecordActionTriggered(); - - /* Record pause (pause/unpause) slot */ - void RecordPauseToggled(); - - /* Replay Buffer action (start/stop) slot */ - void ReplayBufferActionTriggered(); - - /* Virtual Cam action (start/stop) slots */ - void VirtualCamActionTriggered(); - - void OpenVirtualCamConfig(); - - /* Studio Mode toggle slot */ - void TogglePreviewProgramMode(); - -public slots: - void on_actionResetTransform_triggered(); - - bool StreamingActive(); - bool RecordingActive(); - bool ReplayBufferActive(); - bool VirtualCamActive(); - - void ClearContextBar(); - void UpdateContextBar(bool force = false); - void UpdateContextBarDeferred(bool force = false); - void UpdateContextBarVisibility(); + QColor GetSelectionColor() const; signals: - /* Streaming signals */ - void StreamingPreparing(); - void StreamingStarting(bool broadcastAutoStart); - void StreamingStarted(bool withDelay = false); - void StreamingStopping(); - void StreamingStopped(bool withDelay = false); - - /* Broadcast Flow signals */ - void BroadcastFlowEnabled(bool enabled); - void BroadcastStreamReady(bool ready); - void BroadcastStreamActive(); - void BroadcastStreamStarted(bool autoStop); - - /* Recording signals */ - void RecordingStarted(bool pausable = false); - void RecordingPaused(); - void RecordingUnpaused(); - void RecordingStopping(); - void RecordingStopped(); - - /* Replay Buffer signals */ - void ReplayBufEnabled(bool enabled); - void ReplayBufStarted(); - void ReplayBufStopping(); - void ReplayBufStopped(); - - /* Virtual Camera signals */ - void VirtualCamEnabled(); - void VirtualCamStarted(); - void VirtualCamStopped(); - - /* Studio Mode signal */ - void PreviewProgramModeChanged(bool enabled); void CanvasResized(uint32_t width, uint32_t height); void OutputResized(uint32_t width, uint32_t height); @@ -1255,35 +863,10 @@ signals: void PreviewXScrollBarMoved(int value); void PreviewYScrollBarMoved(int value); -private: - std::unique_ptr ui; - - QPointer controlsDock; - -public: - /* `undo_s` needs to be declared after `ui` to prevent an uninitialized - * warning for `ui` while initializing `undo_s`. */ - undo_stack undo_s; - - explicit OBSBasic(QWidget *parent = 0); - virtual ~OBSBasic(); - - virtual void OBSInit() override; - - virtual config_t *Config() const override; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const override; - - static void InitBrowserPanelSafeBlock(); -#ifdef YOUTUBE_ENABLED - void NewYouTubeAppDock(); - void DeleteYouTubeAppDock(); - YouTubeAppDock *GetYouTubeAppDock(); -#endif - // MARK: - Generic UI Helper Functions - OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - - // MARK: - OBS Profile Management + /* ------------------------------------- + * MARK: - OBSBasic_Profiles + * ------------------------------------- + */ private: OBSProfileCache profiles{}; @@ -1327,10 +910,162 @@ public slots: bool CreateDuplicateProfile(const QString &name); void DeleteProfile(const QString &profileName); - // MARK: - OBS Scene Collection Management + /* ------------------------------------- + * MARK: - OBSBasic_Projectors + * ------------------------------------- + */ private: + std::vector savedProjectorsArray; + std::vector projectors; + QPointer previewProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + + void UpdateMultiviewProjectorMenu(); + void ClearProjectors(); + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + +private slots: + void OpenSavedProjector(SavedProjectorInfo *info); + void on_multiviewProjectorWindowed_triggered(); + + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + +public: + void OpenSavedProjectors(); + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + /* ------------------------------------- + * MARK: - OBSBasic_Recording + * ------------------------------------- + */ +private: + QPointer diskFullTimer; + bool recordingStopping = false; + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + void AutoRemux(QString input, bool no_show = false); + void UpdateIsRecordingPausable(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + +private slots: + void on_actionShow_Recordings_triggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + +public slots: + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void PauseRecording(); + void UnpauseRecording(); + + void CheckDiskSpaceRemaining(); + + bool RecordingActive(); + +signals: + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_ReplayBuffer + * ------------------------------------- + */ +private: + bool replayBufferStopping = false; + std::string lastReplay; + +public slots: + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + bool ReplayBufferActive(); + +private slots: + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + +signals: + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_SceneCollections + * ------------------------------------- + */ +private: + OBSDataAutoRelease collectionModuleData; + long disableSaving = 1; + bool projectChanged = false; + bool clearingFailed = false; + + QPointer missDialog; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + OBSSceneCollectionCache collections{}; + void DisableRelativeCoordinates(bool disable); + void CreateDefaultScene(bool firstStart); + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void ClearSceneData(); + void LogScenes(); + void SaveProjectNow(); + void ShowMissingFilesDialog(obs_missing_files_t *files); + void SetupNewSceneCollection(const std::string &collectionName); void SetupDuplicateSceneCollection(const std::string &collectionName); void SetupRenameSceneCollection(const std::string &collectionName); @@ -1347,15 +1082,18 @@ private: void RefreshSceneCollections(bool refreshCache = false); void ActivateSceneCollection(const OBSSceneCollection &collection); -public: - inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); - const OBSSceneCollection &GetCurrentSceneCollection() const; + void SaveProjectDeferred(); + void SaveProject(); - std::optional GetSceneCollectionByName(const std::string &collectionName) const; - std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + bool CreateNewSceneCollection(const QString &name); private slots: + void on_actionShowMissingFiles_triggered(); + void on_actionNewSceneCollection_triggered(); void on_actionDupSceneCollection_triggered(); void on_actionRenameSceneCollection_triggered(); @@ -1364,19 +1102,573 @@ private slots: void on_actionExportSceneCollection_triggered(); void on_actionRemigrateSceneCollection_triggered(); -public slots: - bool CreateNewSceneCollection(const QString &name); -}; +public: + inline bool SavingDisabled() const { return disableSaving; } -extern bool cef_js_avail; + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; -class SceneRenameDelegate : public QStyledItemDelegate { - Q_OBJECT + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + + /* ------------------------------------- + * MARK: - OBSBasic_SceneItems + * ------------------------------------- + */ +private: + QPointer sourceProjector; + QPointer renameSource; + + void CreateFirstRunSources(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + int GetTopSelectedSourceItem(); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + + QModelIndexList GetAllSelectedSourceItems(); + + // TODO: Move back to transitions + QMenu *CreateVisibilityTransitionMenu(bool visible); + void CenterSelectedSceneItems(const CenterType ¢erType); + + /* OBS Callbacks */ + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + +private slots: + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void MixerRenameSource(); + + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + + void on_actionEditTransform_triggered(); + + void on_sources_customContextMenuRequested(const QPoint &pos); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_toggleSourceIcons_toggled(bool visible); + + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); public: - SceneRenameDelegate(QObject *parent); - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); -protected: - virtual bool eventFilter(QObject *editor, QEvent *event) override; + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + /* ------------------------------------- + * MARK: - OBSBasic_Scenes + * ------------------------------------- + */ +private: + QPointer sceneProjectorMenu; + QPointer renameScene; + std::atomic currentScene = nullptr; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + +public slots: + void on_actionResetTransform_triggered(); + +private slots: + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionSceneFilters_triggered(); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + void OpenSceneFilters(); + +public: + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + OBSScene GetCurrentScene(); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + /* ------------------------------------- + * MARK: - OBSBasic_Screenshots + * ------------------------------------- + */ +private: + QPointer screenshotData; + std::string lastScreenshot; + +private slots: + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + /* ------------------------------------- + * MARK: - OBSBasic_Service + * ------------------------------------- + */ +private: + OBSService service; + + bool InitService(); + +public: + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + void SaveService(); + bool LoadService(); + + /* ------------------------------------- + * MARK: - OBSBasic_StatusBar + * ------------------------------------- + */ +private: + QPointer cpuUsageTimer; + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + +public: + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + void ShowStatusBarMessage(const QString &message); + + /* ------------------------------------- + * MARK: - OBSBasic_Streaming + * ------------------------------------- + */ +private: + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool streamingStarting = false; + +public slots: + void DisplayStreamStartError(); + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + bool StreamingActive(); + +private slots: + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* ------------------------------------- + * MARK: - OBSBasic_StudioMode + * ------------------------------------- + */ +private: + QPointer studioProgramProjector; + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + QPointer programOptions; + QPointer program; + OBSWeakSource lastProgramScene; + + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + + OBSWeakSource programScene; + volatile bool previewProgramMode = false; + + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + void UpdatePreviewProgramIndicators(); + +private slots: + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void ProgramViewContextMenuRequested(); + + void OpenStudioProgramProjector(); + void OpenStudioProgramWindow(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public: + OBSSource GetProgramSource(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + +signals: + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + + /* ------------------------------------- + * MARK: - OBSBasic_SysTray + * ------------------------------------- + */ +private: + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer trayMenu; + + bool sysTrayMinimizeToTray(); + +private slots: + void IconActivated(QSystemTrayIcon::ActivationReason reason); + +public: + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + /* ------------------------------------- + * MARK: - OBSBasic_Transitions + * ------------------------------------- + */ +private: + std::vector safeModeTransitions; + QPointer transitionButton; + QPointer perSceneTransitionMenu; + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + std::vector quickTransitions; + bool swapScenesMode = true; + + obs_hotkey_id transitionHotkey = 0; + + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource prevFTBSource = nullptr; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + // TODO: Remove orphaned method. + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + QMenu *CreatePerSceneTransitionMenu(); + + void SetCurrentScene(obs_scene_t *scene, bool force = false); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + +public slots: + void SetCurrentScene(OBSSource scene, bool force = false); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + +private slots: + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void TBarChanged(int value); + void TBarReleased(); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + +public: + int GetTransitionDuration(); + int GetTbarPosition(); + + /* ------------------------------------- + * MARK: - OBSBasic_Updater + * ------------------------------------- + */ +private: + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + /* ------------------------------------- + * MARK: - OBSBasic_VirtualCam + * ------------------------------------- + */ +private: + bool vcamEnabled = false; + VCamConfig vcamConfig; + bool restartingVCam = false; + +public slots: + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + bool VirtualCamActive(); + +private slots: + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + +public: + inline bool VCamEnabled() const { return vcamEnabled; } + +signals: + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_VolControl + * ------------------------------------- + */ +private: + std::vector volumes; + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + +private slots: + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void LockVolumeControl(bool lock); + + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + + void StackedMixerAreaContextMenuRequested(); + +public: + void RefreshVolumeColors(); + + /* ------------------------------------- + * MARK: - OBSBasic_YouTube + * ------------------------------------- + */ + +private: + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; + +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; + + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + +public: +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + +public slots: + void SetupBroadcast(); + +signals: + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); }; diff --git a/frontend/widgets/OBSBasicStatusBar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp index 5bcae6f33..e6d3fce66 100644 --- a/frontend/widgets/OBSBasicStatusBar.cpp +++ b/frontend/widgets/OBSBasicStatusBar.cpp @@ -1,14 +1,10 @@ -#include -#include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-status-bar.cpp" -#include "window-basic-main-outputs.hpp" -#include "qt-wrappers.hpp" -#include "platform.hpp" - +#include "OBSBasicStatusBar.hpp" #include "ui_StatusBarWidget.h" +#include + +#include "moc_OBSBasicStatusBar.cpp" + static constexpr int bitrateUpdateSeconds = 2; static constexpr int congestionUpdateSeconds = 4; static constexpr float excellentThreshold = 0.0f; @@ -16,13 +12,6 @@ static constexpr float goodThreshold = 0.3333f; static constexpr float mediocreThreshold = 0.6667f; static constexpr float badThreshold = 1.0f; -StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) -{ - ui->setupUi(this); -} - -StatusBarWidget::~StatusBarWidget() {} - OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) : QStatusBar(parent), excellentPixmap(QIcon(":/res/images/network-excellent.svg").pixmap(QSize(16, 16))), diff --git a/frontend/widgets/OBSBasicStatusBar.hpp b/frontend/widgets/OBSBasicStatusBar.hpp index 38be9e520..6ccc90fcc 100644 --- a/frontend/widgets/OBSBasicStatusBar.hpp +++ b/frontend/widgets/OBSBasicStatusBar.hpp @@ -1,25 +1,13 @@ #pragma once -#include +#include "StatusBarWidget.hpp" + +#include + #include -#include -#include -#include +#include -class Ui_StatusBarWidget; - -class StatusBarWidget : public QWidget { - Q_OBJECT - - friend class OBSBasicStatusBar; - -private: - std::unique_ptr ui; - -public: - StatusBarWidget(QWidget *parent = nullptr); - ~StatusBarWidget(); -}; +class QTimer; class OBSBasicStatusBar : public QStatusBar { Q_OBJECT diff --git a/frontend/widgets/OBSBasic_Browser.cpp b/frontend/widgets/OBSBasic_Browser.cpp index e822bb134..91cbca5b9 100644 --- a/frontend/widgets/OBSBasic_Browser.cpp +++ b/frontend/widgets/OBSBasic_Browser.cpp @@ -15,23 +15,28 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include "window-basic-main.hpp" - -#include +#include "OBSBasic.hpp" #ifdef BROWSER_AVAILABLE -#include +#include +#include + +#include +#include + +#include + +using namespace json11; #endif +#include + struct QCef; struct QCefCookieManager; -extern QCef *cef; -extern QCefCookieManager *panel_cookies; +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; static std::string GenId() { diff --git a/frontend/widgets/OBSBasic_Clipboard.cpp b/frontend/widgets/OBSBasic_Clipboard.cpp index 47cac8dcc..655591fb7 100644 --- a/frontend/widgets/OBSBasic_Clipboard.cpp +++ b/frontend/widgets/OBSBasic_Clipboard.cpp @@ -16,7908 +16,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include +#include +#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} +extern void undo_redo(const std::string &data); void OBSBasic::on_actionCopyTransform_triggered() { @@ -7930,15 +36,6 @@ void OBSBasic::on_actionCopyTransform_triggered() hasCopiedTransform = true; } -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - void OBSBasic::on_actionPasteTransform_triggered() { OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); @@ -7968,1187 +65,6 @@ void OBSBasic::on_actionPasteTransform_triggered() undo_redo, undo_data, redo_data); } -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - void OBSBasic::on_actionCopySource_triggered() { clipboard.clear(); @@ -9331,873 +247,3 @@ void OBSBasic::on_actionPasteFilters_triggered() SourcePasteFilters(source.Get(), dstSource); } - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_ContextToolbar.cpp b/frontend/widgets/OBSBasic_ContextToolbar.cpp index 47cac8dcc..ccb3474a3 100644 --- a/frontend/widgets/OBSBasic_ContextToolbar.cpp +++ b/frontend/widgets/OBSBasic_ContextToolbar.cpp @@ -16,675 +16,23 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - void OBSBasic::copyActionsDynamicProperties() { // Themes need the QAction dynamic properties @@ -722,2510 +70,6 @@ void OBSBasic::copyActionsDynamicProperties() } } -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - void OBSBasic::ClearContextBar() { QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); @@ -3431,5426 +275,6 @@ void OBSBasic::UpdateContextBar(bool force) } } -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - void OBSBasic::ShowContextBar() { on_toggleContextBar_toggled(true); @@ -8869,1335 +293,3 @@ void OBSBasic::on_toggleContextBar_toggled(bool visible) this->ui->contextContainer->setVisible(visible); UpdateContextBar(true); } - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Docks.cpp b/frontend/widgets/OBSBasic_Docks.cpp index 47cac8dcc..c2bdaf7ec 100644 --- a/frontend/widgets/OBSBasic_Docks.cpp +++ b/frontend/widgets/OBSBasic_Docks.cpp @@ -16,251 +16,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - void assignDockToggle(QDockWidget *dock, QAction *action) { auto handleWindowToggle = [action](bool vis) { @@ -300,8395 +60,6 @@ void setupDockAction(QDockWidget *dock) action->connect(action, &QAction::enabledChanged, neverDisable); } -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - void OBSBasic::on_resetDocks_triggered(bool force) { /* prune deleted extra docks */ @@ -8823,719 +194,6 @@ void OBSBasic::on_sideDocks_toggled(bool side) } } -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - QAction *OBSBasic::AddDockWidget(QDockWidget *dock) { // Prevent the object name from being changed @@ -9691,513 +349,3 @@ void OBSBasic::RepairCustomExtraDockName() dock->setObjectName(extraCustomDockNames[idx]); } - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp index 0e4713816..3f251951a 100644 --- a/frontend/widgets/OBSBasic_Dropfiles.cpp +++ b/frontend/widgets/OBSBasic_Dropfiles.cpp @@ -1,17 +1,32 @@ -#include -#include -#include -#include +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + #include #include -#include #ifdef _WIN32 #include #endif -#include -#include - -#include "window-basic-main.hpp" +#include using namespace std; diff --git a/frontend/widgets/OBSBasic_Hotkeys.cpp b/frontend/widgets/OBSBasic_Hotkeys.cpp index 47cac8dcc..3be00aeee 100644 --- a/frontend/widgets/OBSBasic_Hotkeys.cpp +++ b/frontend/widgets/OBSBasic_Hotkeys.cpp @@ -16,2563 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} +#include void OBSBasic::InitHotkeys() { @@ -2853,7104 +300,6 @@ void OBSBasic::ClearHotkeys() obs_hotkey_unregister(sourceScreenshotHotkey); } -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - void OBSBasic::ResetStatsHotkey() { const QList list = findChildren(); @@ -9959,245 +308,3 @@ void OBSBasic::ResetStatsHotkey() s->Reset(); } } - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Icons.cpp b/frontend/widgets/OBSBasic_Icons.cpp index eabda2a73..cf59dea7d 100644 --- a/frontend/widgets/OBSBasic_Icons.cpp +++ b/frontend/widgets/OBSBasic_Icons.cpp @@ -1,4 +1,23 @@ -#include +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" QIcon OBSBasic::GetSourceIcon(const char *id) const { diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp index 47cac8dcc..6b3c7958c 100644 --- a/frontend/widgets/OBSBasic_MainControls.cpp +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -16,3018 +16,52 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" +#include "OBSBasicStats.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#endif +#include +#include +#ifdef _WIN32 +#include +#endif +#include +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include +#endif +#include + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include +#include #ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" +#include #endif -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" +extern bool restart; +extern bool restart_safe; +extern volatile long insideEventLoop; +extern bool safe_mode; struct QCef; struct QCefCookieManager; -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; +extern QCef *cef; +extern QCefCookieManager *panel_cookies; -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} +using namespace std; void OBSBasic::CreateInteractionWindow(obs_source_t *source) { @@ -3071,1374 +105,12 @@ void OBSBasic::CreateFiltersWindow(obs_source_t *source) filters->setAttribute(Qt::WA_DeleteOnClose, true); } -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - void OBSBasic::updateCheckFinished() { ui->actionCheckForUpdates->setEnabled(true); ui->actionRepair->setEnabled(true); } -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - void OBSBasic::ResetUI() { bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); @@ -4451,177 +123,6 @@ void OBSBasic::ResetUI() UpdatePreviewProgramIndicators(); } -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - void OBSBasic::CloseDialogs() { QList childDialogs = this->findChildren(); @@ -4660,293 +161,6 @@ void OBSBasic::EnumDialogs() } } -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - void OBSBasic::on_actionRemux_triggered() { if (!remux.isNull()) { @@ -5010,42 +224,6 @@ void OBSBasic::on_actionShowMacPermissions_triggered() #endif } -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - void OBSBasic::on_actionAdvAudioProperties_triggered() { if (advAudioWindow != nullptr) { @@ -5061,1039 +239,6 @@ void OBSBasic::on_actionAdvAudioProperties_triggered() advAudioWindow->SetIconsVisible(iconsVisible); } -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - static BPtr ReadLogFile(const char *subdir, const char *log) { char logDir[512]; @@ -6261,1321 +406,6 @@ void OBSBasic::openLogDialog(const QString &text, const bool crash) logDialog.exec(); } -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - void OBSBasic::on_actionHelpPortal_triggered() { QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); @@ -7639,56 +469,6 @@ void OBSBasic::on_actionShowProfileFolder_triggered() } } -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - void OBSBasic::on_actionAlwaysOnTop_triggered() { #ifndef _WIN32 @@ -7712,203 +492,6 @@ void OBSBasic::ToggleAlwaysOnTop() show(); } -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) { if (transformWindow) @@ -7919,722 +502,6 @@ void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); } -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - void OBSBasic::on_actionFullscreenInterface_triggered() { if (!isFullScreen()) @@ -8643,186 +510,6 @@ void OBSBasic::on_actionFullscreenInterface_triggered() showNormal(); } -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - void OBSBasic::on_resetUI_triggered() { on_resetDocks_triggered(); @@ -8837,11 +524,6 @@ void OBSBasic::on_resetUI_triggered() config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); } -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) { ui->sourcesToolbar->setVisible(visible); @@ -8851,25 +533,6 @@ void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); } -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - void OBSBasic::on_toggleStatusBar_toggled(bool visible) { ui->statusbar->setVisible(visible); @@ -8877,69 +540,6 @@ void OBSBasic::on_toggleStatusBar_toggled(bool visible) config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); } -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - void OBSBasic::SetShowing(bool showing) { if (!showing && isVisible()) { @@ -9014,131 +614,6 @@ void OBSBasic::ToggleShowHide() SetShowing(!showing); } -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - void OBSBasic::on_actionMainUndo_triggered() { undo_s.undo(); @@ -9149,327 +624,6 @@ void OBSBasic::on_actionMainRedo_triggered() undo_s.redo(); } -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - void OBSBasic::on_autoConfigure_triggered() { AutoConfig test(this); @@ -9503,463 +657,6 @@ void OBSBasic::on_actionShowAbout_triggered() about->setAttribute(Qt::WA_DeleteOnClose, true); } -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) { QWidget *widget = childAt(pos); @@ -9983,221 +680,3 @@ void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) ui->menuDocks->exec(globalPos); } } - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_OutputHandler.cpp b/frontend/widgets/OBSBasic_OutputHandler.cpp index 47cac8dcc..aa1778f53 100644 --- a/frontend/widgets/OBSBasic_OutputHandler.cpp +++ b/frontend/widgets/OBSBasic_OutputHandler.cpp @@ -16,1978 +16,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; +#include void OBSBasic::ResetOutputs() { @@ -2013,2357 +47,6 @@ void OBSBasic::ResetOutputs() } } -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - bool OBSBasic::Active() const { if (!outputHandler) @@ -4371,5138 +54,6 @@ bool OBSBasic::Active() const return outputHandler->Active(); } -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - void OBSBasic::ResizeOutputSizeOfSource() { if (obs_video_active()) @@ -9536,337 +87,6 @@ void OBSBasic::ResizeOutputSizeOfSource() on_actionFitToScreen_triggered(); } -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - const char *OBSBasic::GetCurrentOutputPath() { const char *path = nullptr; @@ -9917,287 +137,3 @@ bool OBSBasic::OutputPathValid() const char *path = GetCurrentOutputPath(); return path && *path && QDir(path).exists(); } - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp index 47cac8dcc..f9f334ac6 100644 --- a/frontend/widgets/OBSBasic_Preview.cpp +++ b/frontend/widgets/OBSBasic_Preview.cpp @@ -16,1927 +16,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#include + #include -#include -#include -#include -#include -#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include #include -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif +extern void undo_redo(const std::string &data); using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - void OBSBasic::InitPrimitives() { ProfileScope("OBSBasic::InitPrimitives"); @@ -1981,1035 +76,6 @@ void OBSBasic::InitPrimitives() obs_leave_graphics(); } -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - void OBSBasic::UpdatePreviewScalingMenu() { bool fixedScaling = ui->preview->IsFixedScaling(); @@ -3029,1213 +95,6 @@ void OBSBasic::UpdatePreviewScalingMenu() ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); } -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - void OBSBasic::DrawBackdrop(float cx, float cy) { if (!box) @@ -4341,253 +200,6 @@ void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) GS_DEBUG_MARKER_END(); } -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) { QSize targetSize; @@ -4622,3054 +234,11 @@ void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) previewY += float(PREVIEW_EDGE_SIZE); } -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - void OBSBasic::on_preview_customContextMenuRequested() { CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); } -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() { QMenu popup(this); @@ -7689,679 +258,6 @@ void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() popup.exec(QCursor::pos()); } -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - void OBSBasic::EnablePreviewDisplay(bool enable) { obs_display_set_enabled(ui->preview->GetDisplay(), enable); @@ -8393,16 +289,6 @@ void OBSBasic::DisablePreview() EnablePreviewDisplay(false); } -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) { if (obs_sceneitem_locked(item)) @@ -8488,404 +374,6 @@ void OBSBasic::Nudge(int dist, MoveDir dir) obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); } -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - void OBSBasic::on_actionLockPreview_triggered() { ui->preview->ToggleLocked(); @@ -8940,398 +428,6 @@ void OBSBasic::on_actionScaleOutput_triggered() emit ui->preview->DisplayResized(); } -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) { for (int x = 0; x < selectedItems.count(); x++) { @@ -9453,537 +549,6 @@ void OBSBasic::ColorChange() } } -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - void OBSBasic::UpdateProjectorHideCursor() { for (size_t i = 0; i < projectors.size(); i++) @@ -10004,35 +569,6 @@ void OBSBasic::ResetProjectors() OpenSavedProjectors(); } -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - void OBSBasic::UpdatePreviewSafeAreas() { drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); @@ -10049,35 +585,6 @@ void OBSBasic::UpdatePreviewOverflowSettings() ui->preview->SetOverflowAlwaysVisible(always); } -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - static inline QColor color_from_int(long long val) { return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); @@ -10120,12 +627,6 @@ float OBSBasic::GetDevicePixelRatio() return dpi; } -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - void OBSBasic::UpdatePreviewScrollbars() { if (!ui->preview->IsFixedScaling()) { @@ -10158,46 +659,3 @@ void OBSBasic::PreviewScalingModeChanged(int value) break; }; } - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp index 9b346e8d4..2e33dbcd1 100644 --- a/frontend/widgets/OBSBasic_Profiles.cpp +++ b/frontend/widgets/OBSBasic_Profiles.cpp @@ -15,21 +15,17 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#ifdef YOUTUBE_ENABLED +#include +#endif +#include + #include -#include "window-basic-main.hpp" -#include "window-basic-auto-config.hpp" -#include "window-namedialog.hpp" + +#include +#include // MARK: Constant Expressions @@ -38,6 +34,8 @@ constexpr std::string_view OBSProfileSettingsFile = "basic.ini"; // MARK: Forward Declarations +extern bool restart; + extern void DestroyPanelCookieManager(); extern void DuplicateCurrentCookieProfile(ConfigFile &config); extern void CheckExistingCookieId(); diff --git a/frontend/widgets/OBSBasic_Projectors.cpp b/frontend/widgets/OBSBasic_Projectors.cpp index 47cac8dcc..4ffe09bac 100644 --- a/frontend/widgets/OBSBasic_Projectors.cpp +++ b/frontend/widgets/OBSBasic_Projectors.cpp @@ -16,770 +16,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" obs_data_array_t *OBSBasic::SaveProjectors() { @@ -822,211 +61,6 @@ obs_data_array_t *OBSBasic::SaveProjectors() return savedProjectors; } -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) { for (SavedProjectorInfo *info : savedProjectorsArray) { @@ -1051,3615 +85,12 @@ void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) } } -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - void OBSBasic::UpdateMultiviewProjectorMenu() { ui->multiviewProjectorMenu->clear(); AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); } -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - void OBSBasic::ClearProjectors() { for (size_t i = 0; i < projectors.size(); i++) { @@ -4670,455 +101,6 @@ void OBSBasic::ClearProjectors() projectors.clear(); } -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - QList OBSBasic::GetProjectorMenuMonitorsFormatted() { QList projectorsFormatted; @@ -5150,3344 +132,6 @@ QList OBSBasic::GetProjectorMenuMonitorsFormatted() return projectorsFormatted; } -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - void OBSBasic::DeleteProjector(OBSProjector *projector) { for (size_t i = 0; i < projectors.size(); i++) { @@ -8522,12 +166,6 @@ OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, Project return projector; } -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - void OBSBasic::OpenPreviewProjector() { int monitor = sender()->property("monitor").toInt(); @@ -8560,11 +198,6 @@ void OBSBasic::OpenSceneProjector() OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); } -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - void OBSBasic::OpenPreviewWindow() { OpenProjector(nullptr, -1, ProjectorType::Preview); @@ -8635,1569 +268,7 @@ void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) } } -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - void OBSBasic::on_multiviewProjectorWindowed_triggered() { OpenProjector(nullptr, -1, ProjectorType::Multiview); } - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Recording.cpp b/frontend/widgets/OBSBasic_Recording.cpp index 47cac8dcc..83f030488 100644 --- a/frontend/widgets/OBSBasic_Recording.cpp +++ b/frontend/widgets/OBSBasic_Recording.cpp @@ -16,4924 +16,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#include + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} +#include +#include void OBSBasic::on_actionShow_Recordings_triggered() { @@ -4947,2027 +39,11 @@ void OBSBasic::on_actionShow_Recordings_triggered() QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - #define RECORDING_START "==== Recording Start ===============================================" #define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; extern volatile bool replaybuf_active; -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - void OBSBasic::AutoRemux(QString input, bool no_show) { auto config = Config(); @@ -7165,324 +241,6 @@ void OBSBasic::RecordingFileChanged(QString lastRecordingPath) AutoRemux(lastRecordingPath, true); } -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - void OBSBasic::RecordActionTriggered() { if (outputHandler->RecordingActive()) { @@ -7505,2205 +263,6 @@ void OBSBasic::RecordActionTriggered() } } -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - bool OBSBasic::RecordingActive() { if (!outputHandler) @@ -9711,58 +270,6 @@ bool OBSBasic::RecordingActive() return outputHandler->RecordingActive(); } -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - void OBSBasic::PauseRecording() { if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || @@ -9867,57 +374,6 @@ void OBSBasic::UpdateIsRecordingPausable() #define MBYTES_LEFT_STOP_REC 50ULL #define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - void OBSBasic::DiskSpaceMessage() { blog(LOG_ERROR, "Recording stopped because of low disk space"); @@ -9950,254 +406,3 @@ void OBSBasic::CheckDiskSpaceRemaining() DiskSpaceMessage(); } } - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_ReplayBuffer.cpp b/frontend/widgets/OBSBasic_ReplayBuffer.cpp index 47cac8dcc..8d95f2210 100644 --- a/frontend/widgets/OBSBasic_ReplayBuffer.cpp +++ b/frontend/widgets/OBSBasic_ReplayBuffer.cpp @@ -16,1970 +16,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include + #include -#include -#include -#include -#include -#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" void OBSBasic::ReplayBufferActionTriggered() { @@ -1989,5182 +36,6 @@ void OBSBasic::ReplayBufferActionTriggered() StartReplayBuffer(); }; -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - void OBSBasic::ShowReplayBufferPauseWarning() { auto msgBox = []() { @@ -7339,2865 +210,9 @@ void OBSBasic::ReplayBufferStop(int code) OnDeactivate(); } -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - bool OBSBasic::ReplayBufferActive() { if (!outputHandler) return false; return outputHandler->ReplayBufferActive(); } - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp index 47cac8dcc..f28e3d07c 100644 --- a/frontend/widgets/OBSBasic_SceneItems.cpp +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -16,919 +16,25 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" +#include "ColorSelect.hpp" +#include "OBSProjector.hpp" +#include "VolControl.hpp" + +#include +#include +#include +#include + #include -#include -#include -#include -#include -#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include #include -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - static inline bool HasAudioDevices(const char *source_id) { const char *output_id = source_id; @@ -966,2040 +72,6 @@ void OBSBasic::CreateFirstRunSources() ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); } -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) { return item ? GetOBSRef(item) : nullptr; @@ -3010,195 +82,6 @@ OBSSceneItem OBSBasic::GetCurrentSceneItem() return ui->sources->Get(GetTopSelectedSourceItem()); } -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) { QList items = listWidget->findItems(prevName, Qt::MatchExactly); @@ -3226,225 +109,6 @@ void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName UpdatePreviewProgramIndicators(); } -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - void OBSBasic::GetAudioSourceFilters() { QAction *action = reinterpret_cast(sender()); @@ -3463,56 +127,6 @@ void OBSBasic::GetAudioSourceProperties() CreatePropertiesWindow(source); } -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - void OBSBasic::MixerRenameSource() { QAction *action = reinterpret_cast(sender()); @@ -3545,187 +159,6 @@ void OBSBasic::MixerRenameSource() } } -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - void OBSBasic::ActivateAudioSource(OBSSource source) { if (SourceMixerHidden(source)) @@ -3813,316 +246,6 @@ bool OBSBasic::QueryRemoveSource(obs_source_t *source) return Yes == remove_source.clickedButton(); } -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - void OBSBasic::ReorderSources(OBSScene scene) { if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) @@ -4141,35 +264,6 @@ void OBSBasic::RefreshSources(OBSScene scene) SaveProject(); } -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - void OBSBasic::SourceCreated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); @@ -4236,326 +330,6 @@ void OBSBasic::SourceRenamed(void *data, calldata_t *params) blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); } -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - extern char *get_new_source_name(const char *name, const char *format); void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) @@ -4588,785 +362,6 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, cons } } -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - void OBSBasic::SetDeinterlacingMode() { QAction *action = reinterpret_cast(sender()); @@ -5565,11 +560,6 @@ QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction return menu; } -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) { QMenu popup(this); @@ -5725,20 +715,6 @@ void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) } } -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - static inline bool should_show_properties(obs_source_t *source, const char *id) { if (!source) @@ -5878,82 +854,6 @@ static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) return true; }; -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - void OBSBasic::on_actionRemoveSource_triggered() { vector items; @@ -6040,30 +940,6 @@ void OBSBasic::on_actionSourceProperties_triggered() CreatePropertiesWindow(source); } -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - void OBSBasic::on_actionSourceUp_triggered() { MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); @@ -6094,226 +970,6 @@ void OBSBasic::on_actionMoveToBottom_triggered() MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); } -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - void OBSBasic::OpenFilters(OBSSource source) { if (source == nullptr) { @@ -6350,1295 +1006,6 @@ void OBSBasic::OpenEditTransform(OBSSceneItem item) CreateEditTransformWindow(item); } -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - int OBSBasic::GetTopSelectedSourceItem() { QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); @@ -7650,257 +1017,6 @@ QModelIndexList OBSBasic::GetAllSelectedSourceItems() return ui->sources->selectionModel()->selectedIndexes(); } -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - void OBSBasic::on_actionEditTransform_triggered() { const auto item = GetCurrentSceneItem(); @@ -7909,27 +1025,6 @@ void OBSBasic::on_actionEditTransform_triggered() CreateEditTransformWindow(item); } -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - void undo_redo(const std::string &data) { OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); @@ -7939,81 +1034,6 @@ void undo_redo(const std::string &data) obs_scene_load_transform_states(data.c_str()); } -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) { matrix4 boxTransform; @@ -8362,521 +1382,6 @@ void OBSBasic::on_actionHorizontalCenter_triggered() undo_redo, undo_redo, undo_data, redo_data); } -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - void OBSBasic::on_toggleSourceIcons_toggled(bool visible) { ui->sources->SetIconsVisible(visible); @@ -8886,1124 +1391,6 @@ void OBSBasic::on_toggleSourceIcons_toggled(bool visible) config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); } -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - void OBSBasic::on_sourcePropertiesButton_clicked() { on_actionSourceProperties_triggered(); @@ -10014,190 +1401,7 @@ void OBSBasic::on_sourceFiltersButton_clicked() OpenFilters(); } -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - void OBSBasic::on_sourceInteractButton_clicked() { on_actionInteract_triggered(); } - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Scenes.cpp b/frontend/widgets/OBSBasic_Scenes.cpp index 47cac8dcc..ffc029ce4 100644 --- a/frontend/widgets/OBSBasic_Scenes.cpp +++ b/frontend/widgets/OBSBasic_Scenes.cpp @@ -16,757 +16,33 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" + +#include + #include -#include -#include -#include -#include -#include +#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif +#include using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - namespace { -QPointer obsWhatsNew; - template struct SignalContainer { OBSRef ref; vector> handlers; }; } // namespace -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); Q_DECLARE_METATYPE(obs_order_movement); Q_DECLARE_METATYPE(SignalContainer); -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} +extern void undo_redo(const std::string &data); obs_data_array_t *OBSBasic::SaveSceneListOrder() { @@ -781,225 +57,6 @@ obs_data_array_t *OBSBasic::SaveSceneListOrder() return sceneOrder; } -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) { for (int i = 0; i < lw->count(); i++) { @@ -1027,2052 +84,11 @@ void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) } } -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - OBSScene OBSBasic::GetCurrentScene() { return currentScene.load(); } -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - void OBSBasic::AddScene(OBSSource source) { const char *name = obs_source_get_name(source); @@ -3199,697 +215,6 @@ void OBSBasic::AddSceneItem(OBSSceneItem item) } } -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - void OBSBasic::DuplicateSelectedScene() { OBSScene curScene = GetCurrentScene(); @@ -4123,26 +448,6 @@ void OBSBasic::RemoveSelectedScene() OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); } -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - void OBSBasic::SceneReordered(void *data, calldata_t *params) { OBSBasic *window = static_cast(data); @@ -4170,921 +475,6 @@ void OBSBasic::SceneItemAdded(void *data, calldata_t *params) QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); } -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) { OBSSource source; @@ -5119,37 +509,6 @@ void OBSBasic::EditSceneName() item->setFlags(flags); } -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) { QListWidgetItem *item = ui->scenes->itemAt(pos); @@ -5367,364 +726,6 @@ void OBSBasic::EditSceneItemName() ui->sources->Edit(idx); } -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) { if (!witem) @@ -5739,145 +740,6 @@ void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) } } -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) { OBSDataArrayAutoRelease undo_array = obs_data_array_create(); @@ -5954,92 +816,6 @@ void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData und undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); } -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) { OBSSceneItem item = GetCurrentSceneItem(); @@ -6064,203 +840,6 @@ void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &ac CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); } -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) { const char *prevName = obs_source_get_name(source); @@ -6314,42 +893,6 @@ void OBSBasic::SceneNameEdited(QWidget *editor) OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); } -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - void OBSBasic::OpenSceneFilters() { OBSScene scene = GetCurrentScene(); @@ -6358,1616 +901,6 @@ void OBSBasic::OpenSceneFilters() CreateFiltersWindow(source); } -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) { if (obs_sceneitem_is_group(item)) @@ -8014,1445 +947,6 @@ void OBSBasic::on_actionResetTransform_triggered() obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); } -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) { int i = 0; @@ -9470,550 +964,6 @@ SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) return nullptr; } -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - void OBSBasic::on_actionSceneFilters_triggered() { OBSSource sceneSource = GetCurrentSceneSource(); @@ -10021,183 +971,3 @@ void OBSBasic::on_actionSceneFilters_triggered() if (sceneSource) OpenFilters(sceneSource); } - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Screenshots.cpp b/frontend/widgets/OBSBasic_Screenshots.cpp index a0b3622d1..aee070f97 100644 --- a/frontend/widgets/OBSBasic_Screenshots.cpp +++ b/frontend/widgets/OBSBasic_Screenshots.cpp @@ -15,305 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "window-basic-main.hpp" -#include "screenshot-obj.hpp" +#include "OBSBasic.hpp" + +#include #include -#ifdef _WIN32 -#include -#include -#include -#pragma comment(lib, "windowscodecs.lib") -#endif - -static void ScreenshotTick(void *param, float); - -/* ========================================================================= */ - -ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source)) -{ - obs_add_tick_callback(ScreenshotTick, this); -} - -ScreenshotObj::~ScreenshotObj() -{ - obs_enter_graphics(); - gs_stagesurface_destroy(stagesurf); - gs_texrender_destroy(texrender); - obs_leave_graphics(); - - obs_remove_tick_callback(ScreenshotTick, this); - - if (th.joinable()) { - th.join(); - - if (cx && cy) { - OBSBasic *main = OBSBasic::Get(); - main->ShowStatusBarMessage( - QTStr("Basic.StatusBar.ScreenshotSavedTo").arg(QT_UTF8(path.c_str()))); - - main->lastScreenshot = path; - - main->OnEvent(OBS_FRONTEND_EVENT_SCREENSHOT_TAKEN); - } - } -} - -void ScreenshotObj::Screenshot() -{ - OBSSource source = OBSGetStrongRef(weakSource); - - if (source) { - cx = obs_source_get_width(source); - cy = obs_source_get_height(source); - } else { - obs_video_info ovi; - obs_get_video_info(&ovi); - cx = ovi.base_width; - cy = ovi.base_height; - } - - if (!cx || !cy) { - blog(LOG_WARNING, "Cannot screenshot, invalid target size"); - obs_remove_tick_callback(ScreenshotTick, this); - deleteLater(); - return; - } - -#ifdef _WIN32 - enum gs_color_space space = obs_source_get_color_space(source, 0, nullptr); - if (space == GS_CS_709_EXTENDED) { - /* Convert for JXR */ - space = GS_CS_709_SCRGB; - } -#else - /* Tonemap to SDR if HDR */ - const enum gs_color_space space = GS_CS_SRGB; -#endif - const enum gs_color_format format = gs_get_format_from_space(space); - - texrender = gs_texrender_create(format, GS_ZS_NONE); - stagesurf = gs_stagesurface_create(cx, cy, format); - - if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) { - vec4 zero; - vec4_zero(&zero); - - gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0); - gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f); - - gs_blend_state_push(); - gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); - - if (source) { - obs_source_inc_showing(source); - obs_source_video_render(source); - obs_source_dec_showing(source); - } else { - obs_render_main_texture(); - } - - gs_blend_state_pop(); - gs_texrender_end(texrender); - } -} - -void ScreenshotObj::Download() -{ - gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender)); -} - -void ScreenshotObj::Copy() -{ - uint8_t *videoData = nullptr; - uint32_t videoLinesize = 0; - - if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) { - if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) { - const uint32_t linesize = cx * 8; - half_bytes.reserve(cx * cy * 8); - - for (uint32_t y = 0; y < cy; y++) { - const uint8_t *const line = videoData + (y * videoLinesize); - half_bytes.insert(half_bytes.end(), line, line + linesize); - } - } else { - image = QImage(cx, cy, QImage::Format::Format_RGBX8888); - - int linesize = image.bytesPerLine(); - for (int y = 0; y < (int)cy; y++) - memcpy(image.scanLine(y), videoData + (y * videoLinesize), linesize); - } - - gs_stagesurface_unmap(stagesurf); - } -} - -void ScreenshotObj::Save() -{ - OBSBasic *main = OBSBasic::Get(); - config_t *config = main->Config(); - - const char *mode = config_get_string(config, "Output", "Mode"); - const char *type = config_get_string(config, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") ? config_get_string(config, "AdvOut", "FFFilePath") - : config_get_string(config, "AdvOut", "RecFilePath"); - const char *rec_path = strcmp(mode, "Advanced") ? config_get_string(config, "SimpleOutput", "FilePath") - : adv_path; - - bool noSpace = config_get_bool(config, "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(config, "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(config, "Output", "OverwriteIfExists"); - - const char *ext = half_bytes.empty() ? "png" : "jxr"; - path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists, - GetFormatString(filenameFormat, "Screenshot", nullptr).c_str()); - - th = std::thread([this] { MuxAndFinish(); }); -} - -#ifdef _WIN32 -static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy, IWICBitmapFrameEncode *frameEncode, - IPropertyBag2 *options) -{ - wchar_t lossless[] = L"Lossless"; - PROPBAG2 bag = {}; - bag.pstrName = lossless; - VARIANT value = {}; - value.vt = VT_BOOL; - value.bVal = TRUE; - HRESULT hr = options->Write(1, &bag, &value); - if (FAILED(hr)) - return hr; - - hr = frameEncode->Initialize(options); - if (FAILED(hr)) - return hr; - - hr = frameEncode->SetSize(cx, cy); - if (FAILED(hr)) - return hr; - - hr = frameEncode->SetResolution(72, 72); - if (FAILED(hr)) - return hr; - - WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf; - hr = frameEncode->SetPixelFormat(&pixelFormat); - if (FAILED(hr)) - return hr; - - if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) - return E_FAIL; - - hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels); - if (FAILED(hr)) - return hr; - - hr = frameEncode->Commit(); - if (FAILED(hr)) - return hr; - - return S_OK; -} - -static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy) -{ - Microsoft::WRL::ComPtr factory; - HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(factory.GetAddressOf())); - if (FAILED(hr)) - return hr; - - Microsoft::WRL::ComPtr stream; - hr = factory->CreateStream(stream.GetAddressOf()); - if (FAILED(hr)) - return hr; - - hr = stream->InitializeFromFilename(path, GENERIC_WRITE); - if (FAILED(hr)) - return hr; - - Microsoft::WRL::ComPtr encoder = NULL; - hr = factory->CreateEncoder(GUID_ContainerFormatWmp, NULL, encoder.GetAddressOf()); - if (FAILED(hr)) - return hr; - - hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); - if (FAILED(hr)) - return hr; - - Microsoft::WRL::ComPtr frameEncode; - Microsoft::WRL::ComPtr options; - hr = encoder->CreateNewFrame(frameEncode.GetAddressOf(), options.GetAddressOf()); - if (FAILED(hr)) - return hr; - - hr = SaveJxrImage(path, pixels, cx, cy, frameEncode.Get(), options.Get()); - if (FAILED(hr)) - return hr; - - encoder->Commit(); - return S_OK; -} -#endif // #ifdef _WIN32 - -void ScreenshotObj::MuxAndFinish() -{ - if (half_bytes.empty()) { - image.save(QT_UTF8(path.c_str())); - blog(LOG_INFO, "Saved screenshot to '%s'", path.c_str()); - } else { -#ifdef _WIN32 - wchar_t *path_w = nullptr; - os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w); - if (path_w) { - SaveJxr(path_w, half_bytes.data(), cx, cy); - bfree(path_w); - } -#endif // #ifdef _WIN32 - } - - deleteLater(); -} - -/* ========================================================================= */ - -#define STAGE_SCREENSHOT 0 -#define STAGE_DOWNLOAD 1 -#define STAGE_COPY_AND_SAVE 2 -#define STAGE_FINISH 3 - -static void ScreenshotTick(void *param, float) -{ - ScreenshotObj *data = reinterpret_cast(param); - - if (data->stage == STAGE_FINISH) { - return; - } - - obs_enter_graphics(); - - switch (data->stage) { - case STAGE_SCREENSHOT: - data->Screenshot(); - break; - case STAGE_DOWNLOAD: - data->Download(); - break; - case STAGE_COPY_AND_SAVE: - data->Copy(); - QMetaObject::invokeMethod(data, "Save"); - obs_remove_tick_callback(ScreenshotTick, data); - break; - } - - obs_leave_graphics(); - - data->stage++; -} - void OBSBasic::Screenshot(OBSSource source) { if (!!screenshotData) { diff --git a/frontend/widgets/OBSBasic_Service.cpp b/frontend/widgets/OBSBasic_Service.cpp index 47cac8dcc..ee201bc4d 100644 --- a/frontend/widgets/OBSBasic_Service.cpp +++ b/frontend/widgets/OBSBasic_Service.cpp @@ -16,1477 +16,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} +#include "OBSBasic.hpp" constexpr std::string_view OBSServiceFileName = "service.json"; @@ -1574,2775 +105,6 @@ bool OBSBasic::InitService() return true; } -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - obs_service_t *OBSBasic::GetService() { if (!service) { @@ -4358,5846 +120,3 @@ void OBSBasic::SetService(obs_service_t *newService) service = newService; } } - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_StatusBar.cpp b/frontend/widgets/OBSBasic_StatusBar.cpp index 47cac8dcc..a13229809 100644 --- a/frontend/widgets/OBSBasic_StatusBar.cpp +++ b/frontend/widgets/OBSBasic_StatusBar.cpp @@ -16,10188 +16,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} +#include "OBSBasic.hpp" void OBSBasic::ShowStatusBarMessage(const QString &message) { ui->statusbar->clearMessage(); ui->statusbar->showMessage(message, 10000); } - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Streaming.cpp b/frontend/widgets/OBSBasic_Streaming.cpp index 47cac8dcc..db5a30046 100644 --- a/frontend/widgets/OBSBasic_Streaming.cpp +++ b/frontend/widgets/OBSBasic_Streaming.cpp @@ -16,6356 +16,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#ifdef YOUTUBE_ENABLED +#include +#include +#endif + #include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" #define STREAMING_START "==== Streaming Start ===============================================" #define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" void OBSBasic::DisplayStreamStartError() { @@ -6382,104 +45,6 @@ void OBSBasic::DisplayStreamStartError() QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); } -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - void OBSBasic::StartStreaming() { if (outputHandler->StreamingActive()) @@ -6556,179 +121,6 @@ void OBSBasic::StartStreaming() setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); } -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - void OBSBasic::StopStreaming() { SaveProject(); @@ -6968,445 +360,6 @@ void OBSBasic::StreamingStop(int code, QString last_error) SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); } -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - void OBSBasic::StreamActionTriggered() { if (outputHandler->StreamingActive()) { @@ -7483,2721 +436,9 @@ void OBSBasic::StreamActionTriggered() } } -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - bool OBSBasic::StreamingActive() { if (!outputHandler) return false; return outputHandler->StreamingActive(); } - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_SysTray.cpp b/frontend/widgets/OBSBasic_SysTray.cpp index 47cac8dcc..ef74a2a4c 100644 --- a/frontend/widgets/OBSBasic_SysTray.cpp +++ b/frontend/widgets/OBSBasic_SysTray.cpp @@ -16,9003 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} +extern bool opt_minimize_tray; void OBSBasic::SystemTrayInit() { @@ -9138,1066 +145,3 @@ bool OBSBasic::sysTrayMinimizeToTray() { return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); } - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Updater.cpp b/frontend/widgets/OBSBasic_Updater.cpp index 47cac8dcc..b13221128 100644 --- a/frontend/widgets/OBSBasic_Updater.cpp +++ b/frontend/widgets/OBSBasic_Updater.cpp @@ -16,116 +16,39 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include +#include #ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" +#include #endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - #ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" +#include +#include #endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include +#include #endif -using namespace std; - #ifdef BROWSER_AVAILABLE #include #endif +#include -#include "ui-config.h" +#ifdef _WIN32 +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ +#endif struct QCef; struct QCefCookieManager; -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; +extern QCef *cef; +extern QCefCookieManager *panel_cookies; -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); +using namespace std; namespace { @@ -137,2331 +60,6 @@ template struct SignalContainer { }; } // namespace -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ void OBSBasic::ReceivedIntroJson(const QString &text) { #ifdef WHATSNEW_ENABLED @@ -2568,1253 +166,6 @@ void OBSBasic::ShowWhatsNew(const QString &url) #endif } -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - void OBSBasic::TimedCheckForUpdates() { if (App()->IsUpdaterDisabled()) @@ -3883,6321 +234,3 @@ void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) UNUSED_PARAMETER(manualUpdate); #endif } - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_VirtualCam.cpp b/frontend/widgets/OBSBasic_VirtualCam.cpp index 47cac8dcc..3ccb535e6 100644 --- a/frontend/widgets/OBSBasic_VirtualCam.cpp +++ b/frontend/widgets/OBSBasic_VirtualCam.cpp @@ -16,7329 +16,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include +#include +#include -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" #define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" #define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - void OBSBasic::StartVirtualCam() { if (!outputHandler || !outputHandler->virtualCam) @@ -7407,104 +93,6 @@ void OBSBasic::OnVirtualCamStop(int) QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); } -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - void OBSBasic::VirtualCamActionTriggered() { if (outputHandler->VirtualCamActive()) { @@ -7576,2628 +164,9 @@ void OBSBasic::RestartingVirtualCam() restartingVCam = false; } -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - bool OBSBasic::VirtualCamActive() { if (!outputHandler) return false; return outputHandler->VirtualCamActive(); } - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_VolControl.cpp b/frontend/widgets/OBSBasic_VolControl.cpp index 47cac8dcc..674d84829 100644 --- a/frontend/widgets/OBSBasic_VolControl.cpp +++ b/frontend/widgets/OBSBasic_VolControl.cpp @@ -16,712 +16,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif +#include using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - void OBSBasic::UpdateVolumeControlsDecayRate() { double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); @@ -768,2701 +69,6 @@ void OBSBasic::RefreshVolumeColors() } } -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - void OBSBasic::HideAudioControl() { QAction *action = reinterpret_cast(sender()); @@ -3513,46 +119,6 @@ void OBSBasic::ToggleHideMixer() } } -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - void OBSBasic::LockVolumeControl(bool lock) { QAction *action = reinterpret_cast(sender()); @@ -3726,1341 +292,6 @@ void OBSBasic::ToggleVolControlLayout() ActivateAudioSource(source); } -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() { on_actionAdvAudioProperties_triggered(); @@ -5084,5120 +315,3 @@ void OBSBasic::on_actionMixerToolbarMenu_triggered() popup.addAction(&toggleControlLayoutAction); popup.exec(QCursor::pos()); } - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_YouTube.cpp b/frontend/widgets/OBSBasic_YouTube.cpp index 47cac8dcc..bd708bb3f 100644 --- a/frontend/widgets/OBSBasic_YouTube.cpp +++ b/frontend/widgets/OBSBasic_YouTube.cpp @@ -16,6371 +16,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ -#include "ui-config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" #ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" +#include +#include +#include #endif -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif +#include using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} +extern bool cef_js_avail; #ifdef YOUTUBE_ENABLED void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, @@ -6480,82 +129,6 @@ void OBSBasic::ShowYouTubeAutoStartWarning() } #endif -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - void OBSBasic::BroadcastButtonClicked() { if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { @@ -6637,1157 +210,6 @@ void OBSBasic::SetupBroadcast() #endif } -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - #ifdef YOUTUBE_ENABLED YouTubeAppDock *OBSBasic::GetYouTubeAppDock() { @@ -7830,2374 +252,3 @@ void OBSBasic::DeleteYouTubeAppDock() youtubeAppDock = nullptr; } #endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSQTDisplay.cpp b/frontend/widgets/OBSQTDisplay.cpp index f7c37ae9a..4f0d81fb3 100644 --- a/frontend/widgets/OBSQTDisplay.cpp +++ b/frontend/widgets/OBSQTDisplay.cpp @@ -1,57 +1,24 @@ -#include "moc_qt-display.cpp" -#include "display-helpers.hpp" -#include -#include -#include -#include +#include "OBSQTDisplay.hpp" -#include -#include +#include +#include + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#endif + +#include +#ifdef ENABLE_WAYLAND +#include +#include +#endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #endif -#if !defined(_WIN32) && !defined(__APPLE__) -#include -#endif - -#ifdef ENABLE_WAYLAND -#include -#endif - -class SurfaceEventFilter : public QObject { - OBSQTDisplay *display; - -public: - SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) override - { - bool result = QObject::eventFilter(obj, event); - QPlatformSurfaceEvent *surfaceEvent; - - switch (event->type()) { - case QEvent::PlatformSurface: - surfaceEvent = static_cast(event); - - switch (surfaceEvent->surfaceEventType()) { - case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: - display->DestroyDisplay(); - break; - default: - break; - } - break; - default: - break; - } - - return result; - } -}; +#include "moc_OBSQTDisplay.cpp" static inline long long color_to_int(const QColor &color) { diff --git a/frontend/widgets/OBSQTDisplay.hpp b/frontend/widgets/OBSQTDisplay.hpp index e83505a22..4c1152ba5 100644 --- a/frontend/widgets/OBSQTDisplay.hpp +++ b/frontend/widgets/OBSQTDisplay.hpp @@ -1,8 +1,9 @@ #pragma once -#include #include +#include + #define GREY_COLOR_BACKGROUND 0xFF4C4C4C class OBSQTDisplay : public QWidget { diff --git a/frontend/widgets/StatusBarWidget.cpp b/frontend/widgets/StatusBarWidget.cpp index 5bcae6f33..c5a30c8b7 100644 --- a/frontend/widgets/StatusBarWidget.cpp +++ b/frontend/widgets/StatusBarWidget.cpp @@ -1,20 +1,6 @@ -#include -#include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-status-bar.cpp" -#include "window-basic-main-outputs.hpp" -#include "qt-wrappers.hpp" -#include "platform.hpp" - +#include "StatusBarWidget.hpp" #include "ui_StatusBarWidget.h" - -static constexpr int bitrateUpdateSeconds = 2; -static constexpr int congestionUpdateSeconds = 4; -static constexpr float excellentThreshold = 0.0f; -static constexpr float goodThreshold = 0.3333f; -static constexpr float mediocreThreshold = 0.6667f; -static constexpr float badThreshold = 1.0f; +#include "moc_StatusBarWidget.cpp" StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) { @@ -22,580 +8,3 @@ StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui:: } StatusBarWidget::~StatusBarWidget() {} - -OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) - : QStatusBar(parent), - excellentPixmap(QIcon(":/res/images/network-excellent.svg").pixmap(QSize(16, 16))), - goodPixmap(QIcon(":/res/images/network-good.svg").pixmap(QSize(16, 16))), - mediocrePixmap(QIcon(":/res/images/network-mediocre.svg").pixmap(QSize(16, 16))), - badPixmap(QIcon(":/res/images/network-bad.svg").pixmap(QSize(16, 16))), - recordingActivePixmap(QIcon(":/res/images/recording-active.svg").pixmap(QSize(16, 16))), - recordingPausePixmap(QIcon(":/res/images/recording-pause.svg").pixmap(QSize(16, 16))), - streamingActivePixmap(QIcon(":/res/images/streaming-active.svg").pixmap(QSize(16, 16))) -{ - congestionArray.reserve(congestionUpdateSeconds); - - statusWidget = new StatusBarWidget(this); - statusWidget->ui->delayInfo->setText(""); - statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); - statusWidget->ui->statusIcon->setPixmap(inactivePixmap); - statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); - statusWidget->ui->streamTime->setDisabled(true); - statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); - statusWidget->ui->recordTime->setDisabled(true); - statusWidget->ui->delayFrame->hide(); - statusWidget->ui->issuesFrame->hide(); - statusWidget->ui->kbps->hide(); - - addPermanentWidget(statusWidget, 1); - setMinimumHeight(statusWidget->height()); - - UpdateIcons(); - connect(App(), &OBSApp::StyleChanged, this, &OBSBasicStatusBar::UpdateIcons); - - messageTimer = new QTimer(this); - messageTimer->setSingleShot(true); - connect(messageTimer, &QTimer::timeout, this, &OBSBasicStatusBar::clearMessage); - - clearMessage(); -} - -void OBSBasicStatusBar::Activate() -{ - if (!active) { - refreshTimer = new QTimer(this); - connect(refreshTimer, &QTimer::timeout, this, &OBSBasicStatusBar::UpdateStatusBar); - - int skipped = video_output_get_skipped_frames(obs_get_video()); - int total = video_output_get_total_frames(obs_get_video()); - - totalStreamSeconds = 0; - totalRecordSeconds = 0; - lastSkippedFrameCount = 0; - startSkippedFrameCount = skipped; - startTotalFrameCount = total; - - refreshTimer->start(1000); - active = true; - - if (streamOutput) { - statusWidget->ui->statusIcon->setPixmap(inactivePixmap); - } - } - - if (streamOutput) { - statusWidget->ui->streamIcon->setPixmap(streamingActivePixmap); - statusWidget->ui->streamTime->setDisabled(false); - statusWidget->ui->issuesFrame->show(); - statusWidget->ui->kbps->show(); - firstCongestionUpdate = true; - } - - if (recordOutput) { - statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); - statusWidget->ui->recordTime->setDisabled(false); - } -} - -void OBSBasicStatusBar::Deactivate() -{ - OBSBasic *main = qobject_cast(parent()); - if (!main) - return; - - if (!streamOutput) { - statusWidget->ui->streamTime->setText(QString("00:00:00")); - statusWidget->ui->streamTime->setDisabled(true); - statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); - statusWidget->ui->statusIcon->setPixmap(inactivePixmap); - statusWidget->ui->delayFrame->hide(); - statusWidget->ui->issuesFrame->hide(); - statusWidget->ui->kbps->hide(); - totalStreamSeconds = 0; - congestionArray.clear(); - disconnected = false; - firstCongestionUpdate = false; - } - - if (!recordOutput) { - statusWidget->ui->recordTime->setText(QString("00:00:00")); - statusWidget->ui->recordTime->setDisabled(true); - statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); - totalRecordSeconds = 0; - } - - if (main->outputHandler && !main->outputHandler->Active()) { - delete refreshTimer; - - statusWidget->ui->delayInfo->setText(""); - statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); - statusWidget->ui->kbps->setText("0 kbps"); - - delaySecTotal = 0; - delaySecStarting = 0; - delaySecStopping = 0; - reconnectTimeout = 0; - active = false; - overloadedNotify = true; - - statusWidget->ui->statusIcon->setPixmap(inactivePixmap); - } -} - -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)); - } - - if (!statusWidget->ui->delayFrame->isVisible()) - statusWidget->ui->delayFrame->show(); - - statusWidget->ui->delayInfo->setText(msg); - } -} - -void OBSBasicStatusBar::UpdateBandwidth() -{ - if (!streamOutput) - return; - - if (++seconds < bitrateUpdateSeconds) - return; - - OBSOutput output = OBSGetStrongRef(streamOutput); - if (!output) - return; - - uint64_t bytesSent = obs_output_get_total_bytes(output); - uint64_t bytesSentTime = os_gettime_ns(); - - if (bytesSent < lastBytesSent) - bytesSent = 0; - if (bytesSent == 0) - lastBytesSent = 0; - - uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; - - double timePassed = double(bytesSentTime - lastBytesSentTime) / 1000000000.0; - - double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0; - - QString text; - text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps"); - - statusWidget->ui->kbps->setText(text); - statusWidget->ui->kbps->setMinimumWidth(statusWidget->ui->kbps->width()); - - if (!statusWidget->ui->kbps->isVisible()) - statusWidget->ui->kbps->show(); - - lastBytesSent = bytesSent; - lastBytesSentTime = bytesSentTime; - seconds = 0; -} - -void OBSBasicStatusBar::UpdateCPUUsage() -{ - OBSBasic *main = qobject_cast(parent()); - if (!main) - return; - - QString text; - text += QString("CPU: ") + QString::number(main->GetCPUUsage(), 'f', 1) + QString("%"); - - statusWidget->ui->cpuUsage->setText(text); - statusWidget->ui->cpuUsage->setMinimumWidth(statusWidget->ui->cpuUsage->width()); - - UpdateCurrentFPS(); -} - -void OBSBasicStatusBar::UpdateCurrentFPS() -{ - struct obs_video_info ovi; - obs_get_video_info(&ovi); - float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den; - - QString text = QString::asprintf("%.2f / %.2f FPS", obs_get_active_fps(), targetFPS); - - statusWidget->ui->fpsCurrent->setText(text); - statusWidget->ui->fpsCurrent->setMinimumWidth(statusWidget->ui->fpsCurrent->width()); -} - -void OBSBasicStatusBar::UpdateStreamTime() -{ - totalStreamSeconds++; - - int seconds = totalStreamSeconds % 60; - int totalMinutes = totalStreamSeconds / 60; - int minutes = totalMinutes % 60; - int hours = totalMinutes / 60; - - QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); - statusWidget->ui->streamTime->setText(text); - if (streamOutput && !statusWidget->ui->streamTime->isEnabled()) - statusWidget->ui->streamTime->setDisabled(false); - - if (reconnectTimeout > 0) { - QString msg = QTStr("Basic.StatusBar.Reconnecting") - .arg(QString::number(retries), QString::number(reconnectTimeout)); - showMessage(msg); - disconnected = true; - statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); - congestionArray.clear(); - reconnectTimeout--; - - } else if (retries > 0) { - 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(); - } -} - -extern volatile bool recording_paused; - -void OBSBasicStatusBar::UpdateRecordTime() -{ - bool paused = os_atomic_load_bool(&recording_paused); - - if (!paused) { - totalRecordSeconds++; - - if (recordOutput && !statusWidget->ui->recordTime->isEnabled()) - statusWidget->ui->recordTime->setDisabled(false); - } else { - statusWidget->ui->recordIcon->setPixmap(streamPauseIconToggle ? recordingPauseInactivePixmap - : recordingPausePixmap); - - streamPauseIconToggle = !streamPauseIconToggle; - } - - UpdateRecordTimeLabel(); -} - -void OBSBasicStatusBar::UpdateRecordTimeLabel() -{ - int seconds = totalRecordSeconds % 60; - int totalMinutes = totalRecordSeconds / 60; - int minutes = totalMinutes % 60; - int hours = totalMinutes / 60; - - QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); - if (os_atomic_load_bool(&recording_paused)) { - text += QStringLiteral(" (PAUSED)"); - } - - statusWidget->ui->recordTime->setText(text); -} - -void OBSBasicStatusBar::UpdateDroppedFrames() -{ - if (!streamOutput) - return; - - OBSOutput output = OBSGetStrongRef(streamOutput); - if (!output) - return; - - int totalDropped = obs_output_get_frames_dropped(output); - int totalFrames = obs_output_get_total_frames(output); - double percent = (double)totalDropped / (double)totalFrames * 100.0; - - if (!totalFrames) - return; - - QString text = QTStr("DroppedFrames"); - text = text.arg(QString::number(totalDropped), QString::number(percent, 'f', 1)); - statusWidget->ui->droppedFrames->setText(text); - - if (!statusWidget->ui->issuesFrame->isVisible()) - statusWidget->ui->issuesFrame->show(); - - /* ----------------------------------- * - * calculate congestion color */ - - float congestion = obs_output_get_congestion(output); - float avgCongestion = (congestion + lastCongestion) * 0.5f; - if (avgCongestion < congestion) - avgCongestion = congestion; - if (avgCongestion > 1.0f) - avgCongestion = 1.0f; - - lastCongestion = congestion; - - if (disconnected) - return; - - bool update = firstCongestionUpdate; - float congestionOverTime = avgCongestion; - - if (congestionArray.size() >= congestionUpdateSeconds) { - congestionOverTime = accumulate(congestionArray.begin(), congestionArray.end(), 0.0f) / - (float)congestionArray.size(); - congestionArray.clear(); - update = true; - } else { - congestionArray.emplace_back(avgCongestion); - } - - if (update) { - if (congestionOverTime <= excellentThreshold + EPSILON) - statusWidget->ui->statusIcon->setPixmap(excellentPixmap); - else if (congestionOverTime <= goodThreshold) - statusWidget->ui->statusIcon->setPixmap(goodPixmap); - else if (congestionOverTime <= mediocreThreshold) - statusWidget->ui->statusIcon->setPixmap(mediocrePixmap); - else if (congestionOverTime <= badThreshold) - statusWidget->ui->statusIcon->setPixmap(badPixmap); - - firstCongestionUpdate = false; - } -} - -void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params) -{ - OBSBasicStatusBar *statusBar = reinterpret_cast(data); - - int seconds = (int)calldata_int(params, "timeout_sec"); - QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds)); -} - -void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *) -{ - OBSBasicStatusBar *statusBar = reinterpret_cast(data); - - QMetaObject::invokeMethod(statusBar, "ReconnectSuccess"); -} - -void OBSBasicStatusBar::Reconnect(int seconds) -{ - OBSBasic *main = qobject_cast(parent()); - - if (!retries) - main->SysTrayNotify(QTStr("Basic.SystemTray.Message.Reconnecting"), QSystemTrayIcon::Warning); - - reconnectTimeout = seconds; - - if (streamOutput) { - OBSOutput output = OBSGetStrongRef(streamOutput); - if (!output) - return; - - delaySecTotal = obs_output_get_active_delay(output); - UpdateDelayMsg(); - - retries++; - } -} - -void OBSBasicStatusBar::ReconnectClear() -{ - retries = 0; - reconnectTimeout = 0; - seconds = -1; - lastBytesSent = 0; - lastBytesSentTime = os_gettime_ns(); - delaySecTotal = 0; - UpdateDelayMsg(); -} - -void OBSBasicStatusBar::ReconnectSuccess() -{ - OBSBasic *main = qobject_cast(parent()); - - QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful"); - showMessage(msg, 4000); - main->SysTrayNotify(msg, QSystemTrayIcon::Information); - ReconnectClear(); - - if (streamOutput) { - OBSOutput output = OBSGetStrongRef(streamOutput); - if (!output) - return; - - delaySecTotal = obs_output_get_active_delay(output); - UpdateDelayMsg(); - disconnected = false; - firstCongestionUpdate = true; - } -} - -void OBSBasicStatusBar::UpdateStatusBar() -{ - OBSBasic *main = qobject_cast(parent()); - - UpdateBandwidth(); - - if (streamOutput) - UpdateStreamTime(); - - if (recordOutput) - UpdateRecordTime(); - - UpdateDroppedFrames(); - - int skipped = video_output_get_skipped_frames(obs_get_video()); - int total = video_output_get_total_frames(obs_get_video()); - - skipped -= startSkippedFrameCount; - total -= startTotalFrameCount; - - int diff = skipped - lastSkippedFrameCount; - double percentage = double(skipped) / double(total) * 100.0; - - if (diff > 10 && percentage >= 0.1f) { - showMessage(QTStr("HighResourceUsage"), 4000); - if (!main->isVisible() && overloadedNotify) { - main->SysTrayNotify(QTStr("HighResourceUsage"), QSystemTrayIcon::Warning); - overloadedNotify = false; - } - } - - lastSkippedFrameCount = skipped; -} - -void OBSBasicStatusBar::StreamDelayStarting(int sec) -{ - OBSBasic *main = qobject_cast(parent()); - if (!main || !main->outputHandler) - return; - - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - streamOutput = OBSGetWeakRef(output); - - delaySecTotal = delaySecStarting = sec; - UpdateDelayMsg(); - Activate(); -} - -void OBSBasicStatusBar::StreamDelayStopping(int sec) -{ - delaySecTotal = delaySecStopping = sec; - UpdateDelayMsg(); -} - -void OBSBasicStatusBar::StreamStarted(obs_output_t *output) -{ - streamOutput = OBSGetWeakRef(output); - - streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect", OBSOutputReconnect, this); - streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect_success", OBSOutputReconnectSuccess, - this); - - retries = 0; - lastBytesSent = 0; - lastBytesSentTime = os_gettime_ns(); - Activate(); -} - -void OBSBasicStatusBar::StreamStopped() -{ - if (streamOutput) { - streamSigs.clear(); - - ReconnectClear(); - streamOutput = nullptr; - clearMessage(); - Deactivate(); - } -} - -void OBSBasicStatusBar::RecordingStarted(obs_output_t *output) -{ - recordOutput = OBSGetWeakRef(output); - Activate(); -} - -void OBSBasicStatusBar::RecordingStopped() -{ - recordOutput = nullptr; - Deactivate(); -} - -void OBSBasicStatusBar::RecordingPaused() -{ - if (recordOutput) { - statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap); - streamPauseIconToggle = true; - } - - UpdateRecordTimeLabel(); -} - -void OBSBasicStatusBar::RecordingUnpaused() -{ - if (recordOutput) { - statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); - } - - UpdateRecordTimeLabel(); -} - -static QPixmap GetPixmap(const QString &filename) -{ - QString path = obs_frontend_is_theme_dark() ? "theme:Dark/" : ":/res/images/"; - return QIcon(path + filename).pixmap(QSize(16, 16)); -} - -void OBSBasicStatusBar::UpdateIcons() -{ - disconnectedPixmap = GetPixmap("network-disconnected.svg"); - inactivePixmap = GetPixmap("network-inactive.svg"); - - streamingInactivePixmap = GetPixmap("streaming-inactive.svg"); - - recordingInactivePixmap = GetPixmap("recording-inactive.svg"); - recordingPauseInactivePixmap = GetPixmap("recording-pause-inactive.svg"); - - bool streaming = obs_frontend_streaming_active(); - - if (!streaming) { - statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); - statusWidget->ui->statusIcon->setPixmap(inactivePixmap); - } else { - if (disconnected) - statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); - } - - bool recording = obs_frontend_recording_active(); - - if (!recording) - statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); -} - -void OBSBasicStatusBar::showMessage(const QString &message, int timeout) -{ - messageTimer->stop(); - - statusWidget->ui->message->setText(message); - - if (timeout) - messageTimer->start(timeout); -} - -void OBSBasicStatusBar::clearMessage() -{ - statusWidget->ui->message->setText(""); -} diff --git a/frontend/widgets/StatusBarWidget.hpp b/frontend/widgets/StatusBarWidget.hpp index 38be9e520..67e803716 100644 --- a/frontend/widgets/StatusBarWidget.hpp +++ b/frontend/widgets/StatusBarWidget.hpp @@ -1,11 +1,8 @@ #pragma once -#include -#include -#include -#include -#include +#include +class OBSBasicStatusBar; class Ui_StatusBarWidget; class StatusBarWidget : public QWidget { @@ -20,100 +17,3 @@ public: StatusBarWidget(QWidget *parent = nullptr); ~StatusBarWidget(); }; - -class OBSBasicStatusBar : public QStatusBar { - Q_OBJECT - -private: - StatusBarWidget *statusWidget = nullptr; - - OBSWeakOutputAutoRelease streamOutput; - std::vector streamSigs; - OBSWeakOutputAutoRelease recordOutput; - bool active = false; - bool overloadedNotify = true; - bool streamPauseIconToggle = false; - bool disconnected = false; - bool firstCongestionUpdate = false; - - std::vector congestionArray; - - int retries = 0; - int totalStreamSeconds = 0; - int totalRecordSeconds = 0; - - int reconnectTimeout = 0; - - int delaySecTotal = 0; - int delaySecStarting = 0; - int delaySecStopping = 0; - - int startSkippedFrameCount = 0; - int startTotalFrameCount = 0; - int lastSkippedFrameCount = 0; - - int seconds = 0; - uint64_t lastBytesSent = 0; - uint64_t lastBytesSentTime = 0; - - QPixmap excellentPixmap; - QPixmap goodPixmap; - QPixmap mediocrePixmap; - QPixmap badPixmap; - QPixmap disconnectedPixmap; - QPixmap inactivePixmap; - - QPixmap recordingActivePixmap; - QPixmap recordingPausePixmap; - QPixmap recordingPauseInactivePixmap; - QPixmap recordingInactivePixmap; - QPixmap streamingActivePixmap; - QPixmap streamingInactivePixmap; - - float lastCongestion = 0.0f; - - QPointer refreshTimer; - QPointer messageTimer; - - obs_output_t *GetOutput(); - - void Activate(); - void Deactivate(); - - void UpdateDelayMsg(); - void UpdateBandwidth(); - void UpdateStreamTime(); - void UpdateRecordTime(); - void UpdateRecordTimeLabel(); - void UpdateDroppedFrames(); - - static void OBSOutputReconnect(void *data, calldata_t *params); - static void OBSOutputReconnectSuccess(void *data, calldata_t *params); - -public slots: - void UpdateCPUUsage(); - - void clearMessage(); - void showMessage(const QString &message, int timeout = 0); - -private slots: - void Reconnect(int seconds); - void ReconnectSuccess(); - void UpdateStatusBar(); - void UpdateCurrentFPS(); - void UpdateIcons(); - -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); - void RecordingStopped(); - void RecordingPaused(); - void RecordingUnpaused(); - - void ReconnectClear(); -}; diff --git a/frontend/widgets/VolControl.cpp b/frontend/widgets/VolControl.cpp index cd00c14d7..ea9ee5c6b 100644 --- a/frontend/widgets/VolControl.cpp +++ b/frontend/widgets/VolControl.cpp @@ -1,29 +1,14 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolControl.hpp" +#include "VolumeMeter.hpp" +#include "OBSBasic.hpp" -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include -using namespace std; +#include -#define FADER_PRECISION 4096.0 - -// Size of the audio indicator in pixels -#define INDICATOR_THICKNESS 3 - -// Padding on top and bottom of vertical meters -#define METER_PADDING 1 - -std::weak_ptr VolumeMeter::updateTimer; +#include "moc_VolControl.cpp" static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) { @@ -404,238 +389,6 @@ VolControl::~VolControl() contextMenu->close(); } -static inline QColor color_from_int(long long val) -{ - QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); - color.setAlpha(255); - - return color; -} - -QColor VolumeMeter::getBackgroundNominalColor() const -{ - return p_backgroundNominalColor; -} - -QColor VolumeMeter::getBackgroundNominalColorDisabled() const -{ - return backgroundNominalColorDisabled; -} - -void VolumeMeter::setBackgroundNominalColor(QColor c) -{ - p_backgroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); - } else { - backgroundNominalColor = p_backgroundNominalColor; - } -} - -void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) -{ - backgroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundWarningColor() const -{ - return p_backgroundWarningColor; -} - -QColor VolumeMeter::getBackgroundWarningColorDisabled() const -{ - return backgroundWarningColorDisabled; -} - -void VolumeMeter::setBackgroundWarningColor(QColor c) -{ - p_backgroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); - } else { - backgroundWarningColor = p_backgroundWarningColor; - } -} - -void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) -{ - backgroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundErrorColor() const -{ - return p_backgroundErrorColor; -} - -QColor VolumeMeter::getBackgroundErrorColorDisabled() const -{ - return backgroundErrorColorDisabled; -} - -void VolumeMeter::setBackgroundErrorColor(QColor c) -{ - p_backgroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); - } else { - backgroundErrorColor = p_backgroundErrorColor; - } -} - -void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) -{ - backgroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundNominalColor() const -{ - return p_foregroundNominalColor; -} - -QColor VolumeMeter::getForegroundNominalColorDisabled() const -{ - return foregroundNominalColorDisabled; -} - -void VolumeMeter::setForegroundNominalColor(QColor c) -{ - p_foregroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); - } else { - foregroundNominalColor = p_foregroundNominalColor; - } -} - -void VolumeMeter::setForegroundNominalColorDisabled(QColor c) -{ - foregroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundWarningColor() const -{ - return p_foregroundWarningColor; -} - -QColor VolumeMeter::getForegroundWarningColorDisabled() const -{ - return foregroundWarningColorDisabled; -} - -void VolumeMeter::setForegroundWarningColor(QColor c) -{ - p_foregroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); - } else { - foregroundWarningColor = p_foregroundWarningColor; - } -} - -void VolumeMeter::setForegroundWarningColorDisabled(QColor c) -{ - foregroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundErrorColor() const -{ - return p_foregroundErrorColor; -} - -QColor VolumeMeter::getForegroundErrorColorDisabled() const -{ - return foregroundErrorColorDisabled; -} - -void VolumeMeter::setForegroundErrorColor(QColor c) -{ - p_foregroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); - } else { - foregroundErrorColor = p_foregroundErrorColor; - } -} - -void VolumeMeter::setForegroundErrorColorDisabled(QColor c) -{ - foregroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getClipColor() const -{ - return clipColor; -} - -void VolumeMeter::setClipColor(QColor c) -{ - clipColor = std::move(c); -} - -QColor VolumeMeter::getMagnitudeColor() const -{ - return magnitudeColor; -} - -void VolumeMeter::setMagnitudeColor(QColor c) -{ - magnitudeColor = std::move(c); -} - -QColor VolumeMeter::getMajorTickColor() const -{ - return majorTickColor; -} - -void VolumeMeter::setMajorTickColor(QColor c) -{ - majorTickColor = std::move(c); -} - -QColor VolumeMeter::getMinorTickColor() const -{ - return minorTickColor; -} - -void VolumeMeter::setMinorTickColor(QColor c) -{ - minorTickColor = std::move(c); -} - -int VolumeMeter::getMeterThickness() const -{ - return meterThickness; -} - -void VolumeMeter::setMeterThickness(int v) -{ - meterThickness = v; - recalculateLayout = true; -} - -qreal VolumeMeter::getMeterFontScaling() const -{ - return meterFontScaling; -} - -void VolumeMeter::setMeterFontScaling(qreal v) -{ - meterFontScaling = v; - recalculateLayout = true; -} - void VolControl::refreshColors() { volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); @@ -645,863 +398,3 @@ void VolControl::refreshColors() volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); } - -qreal VolumeMeter::getMinimumLevel() const -{ - return minimumLevel; -} - -void VolumeMeter::setMinimumLevel(qreal v) -{ - minimumLevel = v; -} - -qreal VolumeMeter::getWarningLevel() const -{ - return warningLevel; -} - -void VolumeMeter::setWarningLevel(qreal v) -{ - warningLevel = v; -} - -qreal VolumeMeter::getErrorLevel() const -{ - return errorLevel; -} - -void VolumeMeter::setErrorLevel(qreal v) -{ - errorLevel = v; -} - -qreal VolumeMeter::getClipLevel() const -{ - return clipLevel; -} - -void VolumeMeter::setClipLevel(qreal v) -{ - clipLevel = v; -} - -qreal VolumeMeter::getMinimumInputLevel() const -{ - return minimumInputLevel; -} - -void VolumeMeter::setMinimumInputLevel(qreal v) -{ - minimumInputLevel = v; -} - -qreal VolumeMeter::getPeakDecayRate() const -{ - return peakDecayRate; -} - -void VolumeMeter::setPeakDecayRate(qreal v) -{ - peakDecayRate = v; -} - -qreal VolumeMeter::getMagnitudeIntegrationTime() const -{ - return magnitudeIntegrationTime; -} - -void VolumeMeter::setMagnitudeIntegrationTime(qreal v) -{ - magnitudeIntegrationTime = v; -} - -qreal VolumeMeter::getPeakHoldDuration() const -{ - return peakHoldDuration; -} - -void VolumeMeter::setPeakHoldDuration(qreal v) -{ - peakHoldDuration = v; -} - -qreal VolumeMeter::getInputPeakHoldDuration() const -{ - return inputPeakHoldDuration; -} - -void VolumeMeter::setInputPeakHoldDuration(qreal v) -{ - inputPeakHoldDuration = v; -} - -void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); - switch (peakMeterType) { - case TRUE_PEAK_METER: - // For true-peak meters EBU has defined the Permitted Maximum, - // taking into account the accuracy of the meter and further - // processing required by lossy audio compression. - // - // The alignment level was not specified, but I've adjusted - // it compared to a sample-peak meter. Incidentally Youtube - // uses this new Alignment Level as the maximum integrated - // loudness of a video. - // - // * Permitted Maximum Level (PML) = -2.0 dBTP - // * Alignment Level (AL) = -13 dBTP - setErrorLevel(-2.0); - setWarningLevel(-13.0); - break; - - case SAMPLE_PEAK_METER: - default: - // For a sample Peak Meter EBU has the following level - // definitions, taking into account inaccuracies of this meter: - // - // * Permitted Maximum Level (PML) = -9.0 dBFS - // * Alignment Level (AL) = -20.0 dBFS - setErrorLevel(-9.0); - setWarningLevel(-20.0); - break; - } -} - -void VolumeMeter::mousePressEvent(QMouseEvent *event) -{ - setFocus(Qt::MouseFocusReason); - event->accept(); -} - -void VolumeMeter::wheelEvent(QWheelEvent *event) -{ - QApplication::sendEvent(focusProxy(), event); -} - -VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) - : QWidget(parent), - obs_volmeter(obs_volmeter), - vertical(vertical) -{ - setAttribute(Qt::WA_OpaquePaintEvent, true); - - // Default meter settings, they only show if - // there is no stylesheet, do not remove. - backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green - backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow - backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red - foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green - foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow - foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red - - backgroundNominalColorDisabled.setRgb(90, 90, 90); - backgroundWarningColorDisabled.setRgb(117, 117, 117); - backgroundErrorColorDisabled.setRgb(65, 65, 65); - foregroundNominalColorDisabled.setRgb(163, 163, 163); - foregroundWarningColorDisabled.setRgb(217, 217, 217); - foregroundErrorColorDisabled.setRgb(113, 113, 113); - - clipColor.setRgb(0xff, 0xff, 0xff); // Bright white - magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black - majorTickColor.setRgb(0x00, 0x00, 0x00); // Black - minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray - minimumLevel = -60.0; // -60 dB - warningLevel = -20.0; // -20 dB - errorLevel = -9.0; // -9 dB - clipLevel = -0.5; // -0.5 dB - minimumInputLevel = -50.0; // -50 dB - peakDecayRate = 11.76; // 20 dB / 1.7 sec - magnitudeIntegrationTime = 0.3; // 99% in 300 ms - peakHoldDuration = 20.0; // 20 seconds - inputPeakHoldDuration = 1.0; // 1 second - meterThickness = 3; // Bar thickness in pixels - meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size - channels = (int)audio_output_get_channels(obs_get_audio()); - - doLayout(); - updateTimerRef = updateTimer.lock(); - if (!updateTimerRef) { - updateTimerRef = std::make_shared(); - updateTimerRef->setTimerType(Qt::PreciseTimer); - updateTimerRef->start(16); - updateTimer = updateTimerRef; - } - - updateTimerRef->AddVolControl(this); -} - -VolumeMeter::~VolumeMeter() -{ - updateTimerRef->RemoveVolControl(this); -} - -void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - uint64_t ts = os_gettime_ns(); - QMutexLocker locker(&dataMutex); - - currentLastUpdateTime = ts; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = magnitude[channelNr]; - currentPeak[channelNr] = peak[channelNr]; - currentInputPeak[channelNr] = inputPeak[channelNr]; - } - - // In case there are more updates then redraws we must make sure - // that the ballistics of peak and hold are recalculated. - locker.unlock(); - calculateBallistics(ts); -} - -inline void VolumeMeter::resetLevels() -{ - currentLastUpdateTime = 0; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = -M_INFINITE; - currentPeak[channelNr] = -M_INFINITE; - currentInputPeak[channelNr] = -M_INFINITE; - - displayMagnitude[channelNr] = -M_INFINITE; - displayPeak[channelNr] = -M_INFINITE; - displayPeakHold[channelNr] = -M_INFINITE; - displayPeakHoldLastUpdateTime[channelNr] = 0; - displayInputPeakHold[channelNr] = -M_INFINITE; - displayInputPeakHoldLastUpdateTime[channelNr] = 0; - } -} - -bool VolumeMeter::needLayoutChange() -{ - int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); - - if (!currentNrAudioChannels) { - struct obs_audio_info oai; - obs_get_audio_info(&oai); - currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; - } - - if (displayNrAudioChannels != currentNrAudioChannels) { - displayNrAudioChannels = currentNrAudioChannels; - recalculateLayout = true; - } - - return recalculateLayout; -} - -// When this is called from the constructor, obs_volmeter_get_nr_channels has not -// yet been called and Q_PROPERTY settings have not yet been read from the -// stylesheet. -inline void VolumeMeter::doLayout() -{ - QMutexLocker locker(&dataMutex); - - if (displayNrAudioChannels) { - int meterSize = std::floor(22 / displayNrAudioChannels); - setMeterThickness(std::clamp(meterSize, 3, 7)); - } - recalculateLayout = false; - - tickFont = font(); - QFontInfo info(tickFont); - tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); - QFontMetrics metrics(tickFont); - if (vertical) { - // Each meter channel is meterThickness pixels wide, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, space to hold our longest label in this font, - // and a few pixels before the fader. - QRect scaleBounds = metrics.boundingRect("-88"); - setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); - } else { - // Each meter channel is meterThickness pixels high, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, and space high enough to hold our label in - // this font, presuming that digits don't have descenders. - setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); - } - - resetLevels(); -} - -inline bool VolumeMeter::detectIdle(uint64_t ts) -{ - double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; - if (timeSinceLastUpdate > 0.5) { - resetLevels(); - return true; - } else { - return false; - } -} - -inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) -{ - if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { - // Attack of peak is immediate. - displayPeak[channelNr] = currentPeak[channelNr]; - } else { - // Decay of peak is 40 dB / 1.7 seconds for Fast Profile - // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) - // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) - float decay = float(peakDecayRate * timeSinceLastRedraw); - displayPeak[channelNr] = - std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); - } - - if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak - // after 20 seconds. - qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > peakHoldDuration) { - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || - !isfinite(displayInputPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak after 1 second. - qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > inputPeakHoldDuration) { - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (!isfinite(displayMagnitude[channelNr])) { - // The statements in the else-leg do not work with - // NaN and infinite displayMagnitude. - displayMagnitude[channelNr] = currentMagnitude[channelNr]; - } else { - // A VU meter will integrate to the new value to 99% in 300 ms. - // The calculation here is very simplified and is more accurate - // with higher frame-rate. - float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * - (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); - displayMagnitude[channelNr] = - std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); - } -} - -inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) -{ - QMutexLocker locker(&dataMutex); - - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) - calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); -} - -void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) -{ - QMutexLocker locker(&dataMutex); - QColor color; - - if (peakHold < minimumInputLevel) - color = backgroundNominalColor; - else if (peakHold < warningLevel) - color = foregroundNominalColor; - else if (peakHold < errorLevel) - color = foregroundWarningColor; - else if (peakHold <= clipLevel) - color = foregroundErrorColor; - else - color = clipColor; - - painter.fillRect(x, y, width, height, color); -} - -void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) -{ - qreal scale = width / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = int(x + width - (i * scale) - 1); - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - QRect textBounds = metrics.boundingRect(str); - int pos; - if (i == 0) { - pos = position - textBounds.width(); - } else { - pos = position - (textBounds.width() / 2); - if (pos < 0) - pos = 0; - } - painter.drawText(pos, y + 4 + metrics.capHeight(), str); - - painter.drawLine(position, y, position, y + 2); - } -} - -void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) -{ - qreal scale = height / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = y + int(i * scale) + METER_PADDING; - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - if (i == 0) { - painter.drawText(x + 10, position + metrics.capHeight(), str); - } else { - painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); - } - - painter.drawLine(x, position, x + 2, position); - } -} - -#define CLIP_FLASH_DURATION_MS 1000 - -inline int VolumeMeter::convertToInt(float number) -{ - constexpr int min = std::numeric_limits::min(); - constexpr int max = std::numeric_limits::max(); - - // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 - if (number >= (float)max) - return max; - else if (number < min) - return min; - else - return int(number); -} - -void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = width / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = x + 0; - int maximumPosition = x + width; - int magnitudePosition = x + width - convertToInt(magnitude * scale); - int peakPosition = x + width - convertToInt(peak * scale); - int peakHoldPosition = x + width - convertToInt(peakHold * scale); - int warningPosition = x + width - convertToInt(warningLevel * scale); - int errorPosition = x + width - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(minimumPosition, y, end, height, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); -} - -void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = height / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = y + 0; - int maximumPosition = y + height; - int magnitudePosition = y + height - convertToInt(magnitude * scale); - int peakPosition = y + height - convertToInt(peak * scale); - int peakHoldPosition = y + height - convertToInt(peakHold * scale); - int warningPosition = y + height - convertToInt(warningLevel * scale); - int errorPosition = y + height - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(x, minimumPosition, width, end, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); -} - -void VolumeMeter::paintEvent(QPaintEvent *event) -{ - uint64_t ts = os_gettime_ns(); - qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; - calculateBallistics(ts, timeSinceLastRedraw); - bool idle = detectIdle(ts); - - QRect widgetRect = rect(); - int width = widgetRect.width(); - int height = widgetRect.height(); - - QPainter painter(this); - - // Paint window background color (as widget is opaque) - QColor background = palette().color(QPalette::ColorRole::Window); - painter.fillRect(event->region().boundingRect(), background); - - if (vertical) - height -= METER_PADDING * 2; - - // timerEvent requests update of the bar(s) only, so we can avoid the - // overhead of repainting the scale and labels. - if (event->region().boundingRect() != getBarRect()) { - if (needLayoutChange()) - doLayout(); - - if (vertical) { - paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, - height - (INDICATOR_THICKNESS + 3)); - } else { - paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, - width - (INDICATOR_THICKNESS + 3)); - } - } - - if (vertical) { - // Invert the Y axis to ease the math - painter.translate(0, height + METER_PADDING); - painter.scale(1, -1); - } - - for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { - - int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; - - if (vertical) - paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, - height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - else - paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), - width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - - if (idle) - continue; - - // By not drawing the input meter boxes the user can - // see that the audio stream has been stopped, without - // having too much visual impact. - if (vertical) - paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, - INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); - else - paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, - meterThickness, displayInputPeakHold[channelNrFixed]); - } - - lastRedrawTime = ts; -} - -QRect VolumeMeter::getBarRect() const -{ - QRect rec = rect(); - if (vertical) - rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); - else - rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); - - return rec; -} - -void VolumeMeter::changeEvent(QEvent *e) -{ - if (e->type() == QEvent::StyleChange) - recalculateLayout = true; - - QWidget::changeEvent(e); -} - -void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) -{ - volumeMeters.push_back(meter); -} - -void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) -{ - volumeMeters.removeOne(meter); -} - -void VolumeMeterTimer::timerEvent(QTimerEvent *) -{ - for (VolumeMeter *meter : volumeMeters) { - if (meter->needLayoutChange()) { - // Tell paintEvent to update layout and paint everything - meter->update(); - } else { - // Tell paintEvent to paint only the bars - meter->update(meter->getBarRect()); - } - } -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) -{ - fad = fader; -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) - : AbsoluteSlider(orientation, parent) -{ - fad = fader; -} - -bool VolumeSlider::getDisplayTicks() const -{ - return displayTicks; -} - -void VolumeSlider::setDisplayTicks(bool display) -{ - displayTicks = display; -} - -void VolumeSlider::paintEvent(QPaintEvent *event) -{ - if (!getDisplayTicks()) { - QSlider::paintEvent(event); - return; - } - - QPainter painter(this); - QColor tickColor(91, 98, 115, 255); - - obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); - - QStyleOptionSlider opt; - initStyleOption(&opt); - - QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - if (orientation() == Qt::Horizontal) { - const int sliderWidth = groove.width() - handle.width(); - - float tickLength = groove.height() * 1.5; - tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); - - float yPos = groove.center().y() - (tickLength / 2) + 1; - - for (int db = -10; db >= -90; db -= 10) { - float tickValue = fader_db_to_def(db); - - float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); - painter.fillRect(xPos, yPos, 1, tickLength, tickColor); - } - } - - if (orientation() == Qt::Vertical) { - const int sliderHeight = groove.height() - handle.height(); - - float tickLength = groove.width() * 1.5; - tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); - - float xPos = groove.center().x() - (tickLength / 2) + 1; - - for (int db = -10; db >= -96; db -= 10) { - float tickValue = fader_db_to_def(db); - - float yPos = - groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); - painter.fillRect(xPos, yPos, tickLength, 1, tickColor); - } - } - - QSlider::paintEvent(event); -} - -VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} - -VolumeSlider *VolumeAccessibleInterface::slider() const -{ - return qobject_cast(object()); -} - -QString VolumeAccessibleInterface::text(QAccessible::Text t) const -{ - if (slider()->isVisible()) { - switch (t) { - case QAccessible::Text::Value: - return currentValue().toString(); - default: - break; - } - } - return QAccessibleWidget::text(t); -} - -QVariant VolumeAccessibleInterface::currentValue() const -{ - QString text; - float db = obs_fader_get_db(slider()->fad); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - return text; -} - -void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) -{ - slider()->setValue(value.toInt()); -} - -QVariant VolumeAccessibleInterface::maximumValue() const -{ - return slider()->maximum(); -} - -QVariant VolumeAccessibleInterface::minimumValue() const -{ - return slider()->minimum(); -} - -QVariant VolumeAccessibleInterface::minimumStepSize() const -{ - return slider()->singleStep(); -} - -QAccessible::Role VolumeAccessibleInterface::role() const -{ - return QAccessible::Role::Slider; -} diff --git a/frontend/widgets/VolControl.hpp b/frontend/widgets/VolControl.hpp index 51c77f247..fe4ba8a20 100644 --- a/frontend/widgets/VolControl.hpp +++ b/frontend/widgets/VolControl.hpp @@ -1,249 +1,15 @@ #pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include "absolute-slider.hpp" -class QPushButton; -class VolumeMeterTimer; -class VolumeSlider; +#include -class VolumeMeter : public QWidget { - Q_OBJECT - Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) - - Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE - setBackgroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE - setBackgroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE - setBackgroundErrorColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE - setForegroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE - setForegroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE - setForegroundErrorColorDisabled DESIGNABLE true) - - Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) - Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) - Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) - Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) - Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) - Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) - - // Levels are denoted in dBFS. - Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) - Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) - Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) - Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) - Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) - - // Rates are denoted in dB/second. - Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) - - // Time in seconds for the VU meter to integrate over. - Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime - DESIGNABLE true) - - // Duration is denoted in seconds. - Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) - Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration - DESIGNABLE true) - - friend class VolControl; - -private: - obs_volmeter_t *obs_volmeter; - static std::weak_ptr updateTimer; - std::shared_ptr updateTimerRef; - - inline void resetLevels(); - inline void doLayout(); - inline bool detectIdle(uint64_t ts); - inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); - inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); - - inline int convertToInt(float number); - void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); - void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintHTicks(QPainter &painter, int x, int y, int width); - void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintVTicks(QPainter &painter, int x, int y, int height); - - QMutex dataMutex; - - bool recalculateLayout = true; - uint64_t currentLastUpdateTime = 0; - float currentMagnitude[MAX_AUDIO_CHANNELS]; - float currentPeak[MAX_AUDIO_CHANNELS]; - float currentInputPeak[MAX_AUDIO_CHANNELS]; - - int displayNrAudioChannels = 0; - float displayMagnitude[MAX_AUDIO_CHANNELS]; - float displayPeak[MAX_AUDIO_CHANNELS]; - float displayPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - float displayInputPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - - QFont tickFont; - QColor backgroundNominalColor; - QColor backgroundWarningColor; - QColor backgroundErrorColor; - QColor foregroundNominalColor; - QColor foregroundWarningColor; - QColor foregroundErrorColor; - - QColor backgroundNominalColorDisabled; - QColor backgroundWarningColorDisabled; - QColor backgroundErrorColorDisabled; - QColor foregroundNominalColorDisabled; - QColor foregroundWarningColorDisabled; - QColor foregroundErrorColorDisabled; - - QColor clipColor; - QColor magnitudeColor; - QColor majorTickColor; - QColor minorTickColor; - - int meterThickness; - qreal meterFontScaling; - - qreal minimumLevel; - qreal warningLevel; - qreal errorLevel; - qreal clipLevel; - qreal minimumInputLevel; - qreal peakDecayRate; - qreal magnitudeIntegrationTime; - qreal peakHoldDuration; - qreal inputPeakHoldDuration; - - QColor p_backgroundNominalColor; - QColor p_backgroundWarningColor; - QColor p_backgroundErrorColor; - QColor p_foregroundNominalColor; - QColor p_foregroundWarningColor; - QColor p_foregroundErrorColor; - - uint64_t lastRedrawTime = 0; - int channels = 0; - bool clipping = false; - bool vertical; - bool muted = false; - -public: - explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); - ~VolumeMeter(); - - void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]); - QRect getBarRect() const; - bool needLayoutChange(); - - QColor getBackgroundNominalColor() const; - void setBackgroundNominalColor(QColor c); - QColor getBackgroundWarningColor() const; - void setBackgroundWarningColor(QColor c); - QColor getBackgroundErrorColor() const; - void setBackgroundErrorColor(QColor c); - QColor getForegroundNominalColor() const; - void setForegroundNominalColor(QColor c); - QColor getForegroundWarningColor() const; - void setForegroundWarningColor(QColor c); - QColor getForegroundErrorColor() const; - void setForegroundErrorColor(QColor c); - - QColor getBackgroundNominalColorDisabled() const; - void setBackgroundNominalColorDisabled(QColor c); - QColor getBackgroundWarningColorDisabled() const; - void setBackgroundWarningColorDisabled(QColor c); - QColor getBackgroundErrorColorDisabled() const; - void setBackgroundErrorColorDisabled(QColor c); - QColor getForegroundNominalColorDisabled() const; - void setForegroundNominalColorDisabled(QColor c); - QColor getForegroundWarningColorDisabled() const; - void setForegroundWarningColorDisabled(QColor c); - QColor getForegroundErrorColorDisabled() const; - void setForegroundErrorColorDisabled(QColor c); - - QColor getClipColor() const; - void setClipColor(QColor c); - QColor getMagnitudeColor() const; - void setMagnitudeColor(QColor c); - QColor getMajorTickColor() const; - void setMajorTickColor(QColor c); - QColor getMinorTickColor() const; - void setMinorTickColor(QColor c); - int getMeterThickness() const; - void setMeterThickness(int v); - qreal getMeterFontScaling() const; - void setMeterFontScaling(qreal v); - qreal getMinimumLevel() const; - void setMinimumLevel(qreal v); - qreal getWarningLevel() const; - void setWarningLevel(qreal v); - qreal getErrorLevel() const; - void setErrorLevel(qreal v); - qreal getClipLevel() const; - void setClipLevel(qreal v); - qreal getMinimumInputLevel() const; - void setMinimumInputLevel(qreal v); - qreal getPeakDecayRate() const; - void setPeakDecayRate(qreal v); - qreal getMagnitudeIntegrationTime() const; - void setMagnitudeIntegrationTime(qreal v); - qreal getPeakHoldDuration() const; - void setPeakHoldDuration(qreal v); - qreal getInputPeakHoldDuration() const; - void setInputPeakHoldDuration(qreal v); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void wheelEvent(QWheelEvent *event) override; - -protected: - void paintEvent(QPaintEvent *event) override; - void changeEvent(QEvent *e) override; -}; - -class VolumeMeterTimer : public QTimer { - Q_OBJECT - -public: - inline VolumeMeterTimer() : QTimer() {} - - void AddVolControl(VolumeMeter *meter); - void RemoveVolControl(VolumeMeter *meter); - -protected: - void timerEvent(QTimerEvent *event) override; - QList volumeMeters; -}; - -class QLabel; +class OBSSourceLabel; +class VolumeMeter; class VolumeSlider; class MuteCheckBox; -class OBSSourceLabel; +class QLabel; +class QPushButton; class VolControl : public QFrame { Q_OBJECT @@ -298,44 +64,3 @@ public: void refreshColors(); }; - -class VolumeSlider : public AbsoluteSlider { - Q_OBJECT - -public: - obs_fader_t *fad; - - VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); - VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); - - bool getDisplayTicks() const; - void setDisplayTicks(bool display); - -private: - bool displayTicks = false; - QColor tickColor; - -protected: - virtual void paintEvent(QPaintEvent *event) override; -}; - -class VolumeAccessibleInterface : public QAccessibleWidget { - -public: - VolumeAccessibleInterface(QWidget *w); - - QVariant currentValue() const; - void setCurrentValue(const QVariant &value); - - QVariant maximumValue() const; - QVariant minimumValue() const; - - QVariant minimumStepSize() const; - -private: - VolumeSlider *slider() const; - -protected: - virtual QAccessible::Role role() const override; - virtual QString text(QAccessible::Text t) const override; -}; diff --git a/frontend/widgets/VolumeAccessibleInterface.cpp b/frontend/widgets/VolumeAccessibleInterface.cpp index cd00c14d7..ce7e496ab 100644 --- a/frontend/widgets/VolumeAccessibleInterface.cpp +++ b/frontend/widgets/VolumeAccessibleInterface.cpp @@ -1,1452 +1,6 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolumeAccessibleInterface.hpp" -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -#define FADER_PRECISION 4096.0 - -// Size of the audio indicator in pixels -#define INDICATOR_THICKNESS 3 - -// Padding on top and bottom of vertical meters -#define METER_PADDING 1 - -std::weak_ptr VolumeMeter::updateTimer; - -static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) -{ - if (muted) - return Qt::Checked; - else if (unassigned) - return Qt::PartiallyChecked; - else - return Qt::Unchecked; -} - -static inline bool IsSourceUnassigned(obs_source_t *source) -{ - uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); - obs_monitoring_type mt = obs_source_get_monitoring_type(source); - - return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; -} - -static void ShowUnassignedWarning(const char *name) -{ - auto msgBox = [=]() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); - msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); -} - -void VolControl::OBSVolumeChanged(void *data, float db) -{ - Q_UNUSED(db); - VolControl *volControl = static_cast(data); - - QMetaObject::invokeMethod(volControl, "VolumeChanged"); -} - -void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - VolControl *volControl = static_cast(data); - - volControl->volMeter->setLevels(magnitude, peak, inputPeak); -} - -void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) -{ - VolControl *volControl = static_cast(data); - bool muted = calldata_bool(calldata, "muted"); - - QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); -} - -void VolControl::VolumeChanged() -{ - slider->blockSignals(true); - slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); - slider->blockSignals(false); - - updateText(); -} - -void VolControl::VolumeMuted(bool muted) -{ - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) -{ - - VolControl *volControl = static_cast(data); - QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); -} - -void VolControl::MixersOrMonitoringChanged() -{ - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::SetMuted(bool) -{ - bool checked = mute->checkState() == Qt::Checked; - bool prev = obs_source_muted(source); - obs_source_set_muted(source, checked); - bool unassigned = IsSourceUnassigned(source); - - if (!checked && unassigned) { - mute->setCheckState(Qt::PartiallyChecked); - /* Show notice about the source no being assigned to any tracks */ - bool has_shown_warning = - config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); - if (!has_shown_warning) - ShowUnassignedWarning(obs_source_get_name(source)); - } - - auto undo_redo = [](const std::string &uuid, bool val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_muted(source, val); - }; - - QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); - - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); -} - -void VolControl::SliderChanged(int vol) -{ - float prev = obs_source_get_volume(source); - - obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); - updateText(); - - auto undo_redo = [](const std::string &uuid, float val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_volume(source, val); - }; - - float val = obs_source_get_volume(source); - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), - std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); -} - -void VolControl::updateText() -{ - QString text; - float db = obs_fader_get_db(obs_fader); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - volLabel->setText(text); - - bool muted = obs_source_muted(source); - const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; - - QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName); - - slider->setAccessibleName(accText); -} - -void VolControl::EmitConfigClicked() -{ - emit ConfigClicked(); -} - -void VolControl::SetMeterDecayRate(qreal q) -{ - volMeter->setPeakDecayRate(q); -} - -void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - volMeter->setPeakMeterType(peakMeterType); -} - -VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) - : source(std::move(source_)), - levelTotal(0.0f), - levelCount(0.0f), - obs_fader(obs_fader_create(OBS_FADER_LOG)), - obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), - vertical(vertical), - contextMenu(nullptr) -{ - nameLabel = new OBSSourceLabel(source); - volLabel = new QLabel(); - mute = new MuteCheckBox(); - - volLabel->setObjectName("volLabel"); - volLabel->setAlignment(Qt::AlignCenter); - -#ifdef __APPLE__ - mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - QString sourceName = obs_source_get_name(source); - setObjectName(sourceName); - - if (showConfig) { - config = new QPushButton(this); - config->setProperty("class", "icon-dots-vert"); - config->setAutoDefault(false); - - config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); - - connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - } - - QVBoxLayout *mainLayout = new QVBoxLayout; - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - if (vertical) { - QHBoxLayout *nameLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QHBoxLayout *volLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QHBoxLayout *meterLayout = new QHBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new VolumeSlider(obs_fader, Qt::Vertical); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - nameLayout->setAlignment(Qt::AlignCenter); - meterLayout->setAlignment(Qt::AlignCenter); - controlLayout->setAlignment(Qt::AlignCenter); - volLayout->setAlignment(Qt::AlignCenter); - - meterFrame->setObjectName("volMeterFrame"); - - nameLayout->setContentsMargins(0, 0, 0, 0); - nameLayout->setSpacing(0); - nameLayout->addWidget(nameLabel); - - controlLayout->setContentsMargins(0, 0, 0, 0); - controlLayout->setSpacing(0); - - // Add Headphone (audio monitoring) widget here - controlLayout->addWidget(mute); - - if (showConfig) { - controlLayout->addWidget(config); - } - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - meterLayout->addWidget(slider); - meterLayout->addWidget(volMeter); - - meterFrame->setLayout(meterLayout); - - volLayout->setContentsMargins(0, 0, 0, 0); - volLayout->setSpacing(0); - volLayout->addWidget(volLabel); - volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); - - mainLayout->addItem(nameLayout); - mainLayout->addItem(volLayout); - mainLayout->addWidget(meterFrame); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - - // Default size can cause clipping of long names in vertical layout. - QFont font = nameLabel->font(); - QFontInfo info(font); - nameLabel->setFont(font); - - setMaximumWidth(110); - } else { - QHBoxLayout *textLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QVBoxLayout *meterLayout = new QVBoxLayout; - QVBoxLayout *buttonLayout = new QVBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - slider = new VolumeSlider(obs_fader, Qt::Horizontal); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - meterFrame->setObjectName("volMeterFrame"); - meterFrame->setLayout(meterLayout); - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - - meterLayout->addWidget(volMeter); - meterLayout->addWidget(slider); - - buttonLayout->setContentsMargins(0, 0, 0, 0); - buttonLayout->setSpacing(0); - - if (showConfig) { - buttonLayout->addWidget(config); - } - buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); - buttonLayout->addWidget(mute); - - controlLayout->addItem(buttonLayout); - controlLayout->addWidget(meterFrame); - - mainLayout->addItem(textLayout); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - } - - setLayout(mainLayout); - - nameLabel->setText(sourceName); - - slider->setMinimum(0); - slider->setMaximum(int(FADER_PRECISION)); - - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - mute->setCheckState(GetCheckState(muted, unassigned)); - volMeter->muted = muted || unassigned; - mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); - obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, - this); - - QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); - QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); - - obs_fader_attach_source(obs_fader, source); - obs_volmeter_attach_source(obs_volmeter, source); - - /* Call volume changed once to init the slider position and label */ - VolumeChanged(); -} - -void VolControl::EnableSlider(bool enable) -{ - slider->setEnabled(enable); -} - -VolControl::~VolControl() -{ - obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.clear(); - - if (contextMenu) - contextMenu->close(); -} - -static inline QColor color_from_int(long long val) -{ - QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); - color.setAlpha(255); - - return color; -} - -QColor VolumeMeter::getBackgroundNominalColor() const -{ - return p_backgroundNominalColor; -} - -QColor VolumeMeter::getBackgroundNominalColorDisabled() const -{ - return backgroundNominalColorDisabled; -} - -void VolumeMeter::setBackgroundNominalColor(QColor c) -{ - p_backgroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); - } else { - backgroundNominalColor = p_backgroundNominalColor; - } -} - -void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) -{ - backgroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundWarningColor() const -{ - return p_backgroundWarningColor; -} - -QColor VolumeMeter::getBackgroundWarningColorDisabled() const -{ - return backgroundWarningColorDisabled; -} - -void VolumeMeter::setBackgroundWarningColor(QColor c) -{ - p_backgroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); - } else { - backgroundWarningColor = p_backgroundWarningColor; - } -} - -void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) -{ - backgroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getBackgroundErrorColor() const -{ - return p_backgroundErrorColor; -} - -QColor VolumeMeter::getBackgroundErrorColorDisabled() const -{ - return backgroundErrorColorDisabled; -} - -void VolumeMeter::setBackgroundErrorColor(QColor c) -{ - p_backgroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - backgroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); - } else { - backgroundErrorColor = p_backgroundErrorColor; - } -} - -void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) -{ - backgroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundNominalColor() const -{ - return p_foregroundNominalColor; -} - -QColor VolumeMeter::getForegroundNominalColorDisabled() const -{ - return foregroundNominalColorDisabled; -} - -void VolumeMeter::setForegroundNominalColor(QColor c) -{ - p_foregroundNominalColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundNominalColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); - } else { - foregroundNominalColor = p_foregroundNominalColor; - } -} - -void VolumeMeter::setForegroundNominalColorDisabled(QColor c) -{ - foregroundNominalColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundWarningColor() const -{ - return p_foregroundWarningColor; -} - -QColor VolumeMeter::getForegroundWarningColorDisabled() const -{ - return foregroundWarningColorDisabled; -} - -void VolumeMeter::setForegroundWarningColor(QColor c) -{ - p_foregroundWarningColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundWarningColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); - } else { - foregroundWarningColor = p_foregroundWarningColor; - } -} - -void VolumeMeter::setForegroundWarningColorDisabled(QColor c) -{ - foregroundWarningColorDisabled = std::move(c); -} - -QColor VolumeMeter::getForegroundErrorColor() const -{ - return p_foregroundErrorColor; -} - -QColor VolumeMeter::getForegroundErrorColorDisabled() const -{ - return foregroundErrorColorDisabled; -} - -void VolumeMeter::setForegroundErrorColor(QColor c) -{ - p_foregroundErrorColor = std::move(c); - - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - foregroundErrorColor = - color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); - } else { - foregroundErrorColor = p_foregroundErrorColor; - } -} - -void VolumeMeter::setForegroundErrorColorDisabled(QColor c) -{ - foregroundErrorColorDisabled = std::move(c); -} - -QColor VolumeMeter::getClipColor() const -{ - return clipColor; -} - -void VolumeMeter::setClipColor(QColor c) -{ - clipColor = std::move(c); -} - -QColor VolumeMeter::getMagnitudeColor() const -{ - return magnitudeColor; -} - -void VolumeMeter::setMagnitudeColor(QColor c) -{ - magnitudeColor = std::move(c); -} - -QColor VolumeMeter::getMajorTickColor() const -{ - return majorTickColor; -} - -void VolumeMeter::setMajorTickColor(QColor c) -{ - majorTickColor = std::move(c); -} - -QColor VolumeMeter::getMinorTickColor() const -{ - return minorTickColor; -} - -void VolumeMeter::setMinorTickColor(QColor c) -{ - minorTickColor = std::move(c); -} - -int VolumeMeter::getMeterThickness() const -{ - return meterThickness; -} - -void VolumeMeter::setMeterThickness(int v) -{ - meterThickness = v; - recalculateLayout = true; -} - -qreal VolumeMeter::getMeterFontScaling() const -{ - return meterFontScaling; -} - -void VolumeMeter::setMeterFontScaling(qreal v) -{ - meterFontScaling = v; - recalculateLayout = true; -} - -void VolControl::refreshColors() -{ - volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); - volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); - volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); - volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); - volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); - volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); -} - -qreal VolumeMeter::getMinimumLevel() const -{ - return minimumLevel; -} - -void VolumeMeter::setMinimumLevel(qreal v) -{ - minimumLevel = v; -} - -qreal VolumeMeter::getWarningLevel() const -{ - return warningLevel; -} - -void VolumeMeter::setWarningLevel(qreal v) -{ - warningLevel = v; -} - -qreal VolumeMeter::getErrorLevel() const -{ - return errorLevel; -} - -void VolumeMeter::setErrorLevel(qreal v) -{ - errorLevel = v; -} - -qreal VolumeMeter::getClipLevel() const -{ - return clipLevel; -} - -void VolumeMeter::setClipLevel(qreal v) -{ - clipLevel = v; -} - -qreal VolumeMeter::getMinimumInputLevel() const -{ - return minimumInputLevel; -} - -void VolumeMeter::setMinimumInputLevel(qreal v) -{ - minimumInputLevel = v; -} - -qreal VolumeMeter::getPeakDecayRate() const -{ - return peakDecayRate; -} - -void VolumeMeter::setPeakDecayRate(qreal v) -{ - peakDecayRate = v; -} - -qreal VolumeMeter::getMagnitudeIntegrationTime() const -{ - return magnitudeIntegrationTime; -} - -void VolumeMeter::setMagnitudeIntegrationTime(qreal v) -{ - magnitudeIntegrationTime = v; -} - -qreal VolumeMeter::getPeakHoldDuration() const -{ - return peakHoldDuration; -} - -void VolumeMeter::setPeakHoldDuration(qreal v) -{ - peakHoldDuration = v; -} - -qreal VolumeMeter::getInputPeakHoldDuration() const -{ - return inputPeakHoldDuration; -} - -void VolumeMeter::setInputPeakHoldDuration(qreal v) -{ - inputPeakHoldDuration = v; -} - -void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); - switch (peakMeterType) { - case TRUE_PEAK_METER: - // For true-peak meters EBU has defined the Permitted Maximum, - // taking into account the accuracy of the meter and further - // processing required by lossy audio compression. - // - // The alignment level was not specified, but I've adjusted - // it compared to a sample-peak meter. Incidentally Youtube - // uses this new Alignment Level as the maximum integrated - // loudness of a video. - // - // * Permitted Maximum Level (PML) = -2.0 dBTP - // * Alignment Level (AL) = -13 dBTP - setErrorLevel(-2.0); - setWarningLevel(-13.0); - break; - - case SAMPLE_PEAK_METER: - default: - // For a sample Peak Meter EBU has the following level - // definitions, taking into account inaccuracies of this meter: - // - // * Permitted Maximum Level (PML) = -9.0 dBFS - // * Alignment Level (AL) = -20.0 dBFS - setErrorLevel(-9.0); - setWarningLevel(-20.0); - break; - } -} - -void VolumeMeter::mousePressEvent(QMouseEvent *event) -{ - setFocus(Qt::MouseFocusReason); - event->accept(); -} - -void VolumeMeter::wheelEvent(QWheelEvent *event) -{ - QApplication::sendEvent(focusProxy(), event); -} - -VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) - : QWidget(parent), - obs_volmeter(obs_volmeter), - vertical(vertical) -{ - setAttribute(Qt::WA_OpaquePaintEvent, true); - - // Default meter settings, they only show if - // there is no stylesheet, do not remove. - backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green - backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow - backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red - foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green - foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow - foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red - - backgroundNominalColorDisabled.setRgb(90, 90, 90); - backgroundWarningColorDisabled.setRgb(117, 117, 117); - backgroundErrorColorDisabled.setRgb(65, 65, 65); - foregroundNominalColorDisabled.setRgb(163, 163, 163); - foregroundWarningColorDisabled.setRgb(217, 217, 217); - foregroundErrorColorDisabled.setRgb(113, 113, 113); - - clipColor.setRgb(0xff, 0xff, 0xff); // Bright white - magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black - majorTickColor.setRgb(0x00, 0x00, 0x00); // Black - minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray - minimumLevel = -60.0; // -60 dB - warningLevel = -20.0; // -20 dB - errorLevel = -9.0; // -9 dB - clipLevel = -0.5; // -0.5 dB - minimumInputLevel = -50.0; // -50 dB - peakDecayRate = 11.76; // 20 dB / 1.7 sec - magnitudeIntegrationTime = 0.3; // 99% in 300 ms - peakHoldDuration = 20.0; // 20 seconds - inputPeakHoldDuration = 1.0; // 1 second - meterThickness = 3; // Bar thickness in pixels - meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size - channels = (int)audio_output_get_channels(obs_get_audio()); - - doLayout(); - updateTimerRef = updateTimer.lock(); - if (!updateTimerRef) { - updateTimerRef = std::make_shared(); - updateTimerRef->setTimerType(Qt::PreciseTimer); - updateTimerRef->start(16); - updateTimer = updateTimerRef; - } - - updateTimerRef->AddVolControl(this); -} - -VolumeMeter::~VolumeMeter() -{ - updateTimerRef->RemoveVolControl(this); -} - -void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - uint64_t ts = os_gettime_ns(); - QMutexLocker locker(&dataMutex); - - currentLastUpdateTime = ts; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = magnitude[channelNr]; - currentPeak[channelNr] = peak[channelNr]; - currentInputPeak[channelNr] = inputPeak[channelNr]; - } - - // In case there are more updates then redraws we must make sure - // that the ballistics of peak and hold are recalculated. - locker.unlock(); - calculateBallistics(ts); -} - -inline void VolumeMeter::resetLevels() -{ - currentLastUpdateTime = 0; - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { - currentMagnitude[channelNr] = -M_INFINITE; - currentPeak[channelNr] = -M_INFINITE; - currentInputPeak[channelNr] = -M_INFINITE; - - displayMagnitude[channelNr] = -M_INFINITE; - displayPeak[channelNr] = -M_INFINITE; - displayPeakHold[channelNr] = -M_INFINITE; - displayPeakHoldLastUpdateTime[channelNr] = 0; - displayInputPeakHold[channelNr] = -M_INFINITE; - displayInputPeakHoldLastUpdateTime[channelNr] = 0; - } -} - -bool VolumeMeter::needLayoutChange() -{ - int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); - - if (!currentNrAudioChannels) { - struct obs_audio_info oai; - obs_get_audio_info(&oai); - currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; - } - - if (displayNrAudioChannels != currentNrAudioChannels) { - displayNrAudioChannels = currentNrAudioChannels; - recalculateLayout = true; - } - - return recalculateLayout; -} - -// When this is called from the constructor, obs_volmeter_get_nr_channels has not -// yet been called and Q_PROPERTY settings have not yet been read from the -// stylesheet. -inline void VolumeMeter::doLayout() -{ - QMutexLocker locker(&dataMutex); - - if (displayNrAudioChannels) { - int meterSize = std::floor(22 / displayNrAudioChannels); - setMeterThickness(std::clamp(meterSize, 3, 7)); - } - recalculateLayout = false; - - tickFont = font(); - QFontInfo info(tickFont); - tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); - QFontMetrics metrics(tickFont); - if (vertical) { - // Each meter channel is meterThickness pixels wide, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, space to hold our longest label in this font, - // and a few pixels before the fader. - QRect scaleBounds = metrics.boundingRect("-88"); - setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); - } else { - // Each meter channel is meterThickness pixels high, plus one pixel - // between channels, but not after the last. - // Add 4 pixels for ticks, and space high enough to hold our label in - // this font, presuming that digits don't have descenders. - setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); - } - - resetLevels(); -} - -inline bool VolumeMeter::detectIdle(uint64_t ts) -{ - double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; - if (timeSinceLastUpdate > 0.5) { - resetLevels(); - return true; - } else { - return false; - } -} - -inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) -{ - if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { - // Attack of peak is immediate. - displayPeak[channelNr] = currentPeak[channelNr]; - } else { - // Decay of peak is 40 dB / 1.7 seconds for Fast Profile - // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) - // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) - float decay = float(peakDecayRate * timeSinceLastRedraw); - displayPeak[channelNr] = - std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); - } - - if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak - // after 20 seconds. - qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > peakHoldDuration) { - displayPeakHold[channelNr] = currentPeak[channelNr]; - displayPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || - !isfinite(displayInputPeakHold[channelNr])) { - // Attack of peak-hold is immediate, but keep track - // when it was last updated. - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } else { - // The peak and hold falls back to peak after 1 second. - qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; - if (timeSinceLastPeak > inputPeakHoldDuration) { - displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; - displayInputPeakHoldLastUpdateTime[channelNr] = ts; - } - } - - if (!isfinite(displayMagnitude[channelNr])) { - // The statements in the else-leg do not work with - // NaN and infinite displayMagnitude. - displayMagnitude[channelNr] = currentMagnitude[channelNr]; - } else { - // A VU meter will integrate to the new value to 99% in 300 ms. - // The calculation here is very simplified and is more accurate - // with higher frame-rate. - float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * - (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); - displayMagnitude[channelNr] = - std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); - } -} - -inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) -{ - QMutexLocker locker(&dataMutex); - - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) - calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); -} - -void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) -{ - QMutexLocker locker(&dataMutex); - QColor color; - - if (peakHold < minimumInputLevel) - color = backgroundNominalColor; - else if (peakHold < warningLevel) - color = foregroundNominalColor; - else if (peakHold < errorLevel) - color = foregroundWarningColor; - else if (peakHold <= clipLevel) - color = foregroundErrorColor; - else - color = clipColor; - - painter.fillRect(x, y, width, height, color); -} - -void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) -{ - qreal scale = width / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = int(x + width - (i * scale) - 1); - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - QRect textBounds = metrics.boundingRect(str); - int pos; - if (i == 0) { - pos = position - textBounds.width(); - } else { - pos = position - (textBounds.width() / 2); - if (pos < 0) - pos = 0; - } - painter.drawText(pos, y + 4 + metrics.capHeight(), str); - - painter.drawLine(position, y, position, y + 2); - } -} - -void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) -{ - qreal scale = height / minimumLevel; - - painter.setFont(tickFont); - QFontMetrics metrics(tickFont); - painter.setPen(majorTickColor); - - // Draw major tick lines and numeric indicators. - for (int i = 0; i >= minimumLevel; i -= 5) { - int position = y + int(i * scale) + METER_PADDING; - QString str = QString::number(i); - - // Center the number on the tick, but don't overflow - if (i == 0) { - painter.drawText(x + 10, position + metrics.capHeight(), str); - } else { - painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); - } - - painter.drawLine(x, position, x + 2, position); - } -} - -#define CLIP_FLASH_DURATION_MS 1000 - -inline int VolumeMeter::convertToInt(float number) -{ - constexpr int min = std::numeric_limits::min(); - constexpr int max = std::numeric_limits::max(); - - // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 - if (number >= (float)max) - return max; - else if (number < min) - return min; - else - return int(number); -} - -void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = width / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = x + 0; - int maximumPosition = x + width; - int magnitudePosition = x + width - convertToInt(magnitude * scale); - int peakPosition = x + width - convertToInt(peak * scale); - int peakHoldPosition = x + width - convertToInt(peakHold * scale); - int warningPosition = x + width - convertToInt(warningLevel * scale); - int errorPosition = x + width - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(errorPosition, y, errorLength, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(minimumPosition, y, nominalLength, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(warningPosition, y, warningLength, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(minimumPosition, y, end, height, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(peakHoldPosition - 3, y, 3, height, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); -} - -void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold) -{ - qreal scale = height / minimumLevel; - - QMutexLocker locker(&dataMutex); - int minimumPosition = y + 0; - int maximumPosition = y + height; - int magnitudePosition = y + height - convertToInt(magnitude * scale); - int peakPosition = y + height - convertToInt(peak * scale); - int peakHoldPosition = y + height - convertToInt(peakHold * scale); - int warningPosition = y + height - convertToInt(warningLevel * scale); - int errorPosition = y + height - convertToInt(errorLevel * scale); - - int nominalLength = warningPosition - minimumPosition; - int warningLength = errorPosition - warningPosition; - int errorLength = maximumPosition - errorPosition; - locker.unlock(); - - if (clipping) { - peakPosition = maximumPosition; - } - - if (peakPosition < minimumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < warningPosition) { - painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, - muted ? backgroundNominalColorDisabled : backgroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < errorPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, - muted ? backgroundWarningColorDisabled : backgroundWarningColor); - painter.fillRect(x, errorPosition, width, errorLength, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else if (peakPosition < maximumPosition) { - painter.fillRect(x, minimumPosition, width, nominalLength, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - painter.fillRect(x, warningPosition, width, warningLength, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, - muted ? backgroundErrorColorDisabled : backgroundErrorColor); - } else { - if (!clipping) { - QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); - clipping = true; - } - - int end = errorLength + warningLength + nominalLength; - painter.fillRect(x, minimumPosition, width, end, - QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); - } - - if (peakHoldPosition - 3 < minimumPosition) - ; // Peak-hold below minimum, no drawing. - else if (peakHoldPosition < warningPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundNominalColorDisabled : foregroundNominalColor); - else if (peakHoldPosition < errorPosition) - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundWarningColorDisabled : foregroundWarningColor); - else - painter.fillRect(x, peakHoldPosition - 3, width, 3, - muted ? foregroundErrorColorDisabled : foregroundErrorColor); - - if (magnitudePosition - 3 >= minimumPosition) - painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); -} - -void VolumeMeter::paintEvent(QPaintEvent *event) -{ - uint64_t ts = os_gettime_ns(); - qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; - calculateBallistics(ts, timeSinceLastRedraw); - bool idle = detectIdle(ts); - - QRect widgetRect = rect(); - int width = widgetRect.width(); - int height = widgetRect.height(); - - QPainter painter(this); - - // Paint window background color (as widget is opaque) - QColor background = palette().color(QPalette::ColorRole::Window); - painter.fillRect(event->region().boundingRect(), background); - - if (vertical) - height -= METER_PADDING * 2; - - // timerEvent requests update of the bar(s) only, so we can avoid the - // overhead of repainting the scale and labels. - if (event->region().boundingRect() != getBarRect()) { - if (needLayoutChange()) - doLayout(); - - if (vertical) { - paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, - height - (INDICATOR_THICKNESS + 3)); - } else { - paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, - width - (INDICATOR_THICKNESS + 3)); - } - } - - if (vertical) { - // Invert the Y axis to ease the math - painter.translate(0, height + METER_PADDING); - painter.scale(1, -1); - } - - for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { - - int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; - - if (vertical) - paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, - height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - else - paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), - width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], - displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); - - if (idle) - continue; - - // By not drawing the input meter boxes the user can - // see that the audio stream has been stopped, without - // having too much visual impact. - if (vertical) - paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, - INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); - else - paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, - meterThickness, displayInputPeakHold[channelNrFixed]); - } - - lastRedrawTime = ts; -} - -QRect VolumeMeter::getBarRect() const -{ - QRect rec = rect(); - if (vertical) - rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); - else - rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); - - return rec; -} - -void VolumeMeter::changeEvent(QEvent *e) -{ - if (e->type() == QEvent::StyleChange) - recalculateLayout = true; - - QWidget::changeEvent(e); -} - -void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) -{ - volumeMeters.push_back(meter); -} - -void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) -{ - volumeMeters.removeOne(meter); -} - -void VolumeMeterTimer::timerEvent(QTimerEvent *) -{ - for (VolumeMeter *meter : volumeMeters) { - if (meter->needLayoutChange()) { - // Tell paintEvent to update layout and paint everything - meter->update(); - } else { - // Tell paintEvent to paint only the bars - meter->update(meter->getBarRect()); - } - } -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) -{ - fad = fader; -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) - : AbsoluteSlider(orientation, parent) -{ - fad = fader; -} - -bool VolumeSlider::getDisplayTicks() const -{ - return displayTicks; -} - -void VolumeSlider::setDisplayTicks(bool display) -{ - displayTicks = display; -} - -void VolumeSlider::paintEvent(QPaintEvent *event) -{ - if (!getDisplayTicks()) { - QSlider::paintEvent(event); - return; - } - - QPainter painter(this); - QColor tickColor(91, 98, 115, 255); - - obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); - - QStyleOptionSlider opt; - initStyleOption(&opt); - - QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - if (orientation() == Qt::Horizontal) { - const int sliderWidth = groove.width() - handle.width(); - - float tickLength = groove.height() * 1.5; - tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); - - float yPos = groove.center().y() - (tickLength / 2) + 1; - - for (int db = -10; db >= -90; db -= 10) { - float tickValue = fader_db_to_def(db); - - float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); - painter.fillRect(xPos, yPos, 1, tickLength, tickColor); - } - } - - if (orientation() == Qt::Vertical) { - const int sliderHeight = groove.height() - handle.height(); - - float tickLength = groove.width() * 1.5; - tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); - - float xPos = groove.center().x() - (tickLength / 2) + 1; - - for (int db = -10; db >= -96; db -= 10) { - float tickValue = fader_db_to_def(db); - - float yPos = - groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); - painter.fillRect(xPos, yPos, tickLength, 1, tickColor); - } - } - - QSlider::paintEvent(event); -} +#include VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} diff --git a/frontend/widgets/VolumeAccessibleInterface.hpp b/frontend/widgets/VolumeAccessibleInterface.hpp index 51c77f247..2de609162 100644 --- a/frontend/widgets/VolumeAccessibleInterface.hpp +++ b/frontend/widgets/VolumeAccessibleInterface.hpp @@ -1,324 +1,9 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include #include -#include "absolute-slider.hpp" -class QPushButton; -class VolumeMeterTimer; class VolumeSlider; -class VolumeMeter : public QWidget { - Q_OBJECT - Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor - DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor - DESIGNABLE true) - Q_PROPERTY( - QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) - - Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE - setBackgroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE - setBackgroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE - setBackgroundErrorColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE - setForegroundNominalColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE - setForegroundWarningColorDisabled DESIGNABLE true) - Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE - setForegroundErrorColorDisabled DESIGNABLE true) - - Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) - Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) - Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) - Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) - Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) - Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) - - // Levels are denoted in dBFS. - Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) - Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) - Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) - Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) - Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) - - // Rates are denoted in dB/second. - Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) - - // Time in seconds for the VU meter to integrate over. - Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime - DESIGNABLE true) - - // Duration is denoted in seconds. - Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) - Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration - DESIGNABLE true) - - friend class VolControl; - -private: - obs_volmeter_t *obs_volmeter; - static std::weak_ptr updateTimer; - std::shared_ptr updateTimerRef; - - inline void resetLevels(); - inline void doLayout(); - inline bool detectIdle(uint64_t ts); - inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); - inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); - - inline int convertToInt(float number); - void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); - void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintHTicks(QPainter &painter, int x, int y, int width); - void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, - float peakHold); - void paintVTicks(QPainter &painter, int x, int y, int height); - - QMutex dataMutex; - - bool recalculateLayout = true; - uint64_t currentLastUpdateTime = 0; - float currentMagnitude[MAX_AUDIO_CHANNELS]; - float currentPeak[MAX_AUDIO_CHANNELS]; - float currentInputPeak[MAX_AUDIO_CHANNELS]; - - int displayNrAudioChannels = 0; - float displayMagnitude[MAX_AUDIO_CHANNELS]; - float displayPeak[MAX_AUDIO_CHANNELS]; - float displayPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - float displayInputPeakHold[MAX_AUDIO_CHANNELS]; - uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; - - QFont tickFont; - QColor backgroundNominalColor; - QColor backgroundWarningColor; - QColor backgroundErrorColor; - QColor foregroundNominalColor; - QColor foregroundWarningColor; - QColor foregroundErrorColor; - - QColor backgroundNominalColorDisabled; - QColor backgroundWarningColorDisabled; - QColor backgroundErrorColorDisabled; - QColor foregroundNominalColorDisabled; - QColor foregroundWarningColorDisabled; - QColor foregroundErrorColorDisabled; - - QColor clipColor; - QColor magnitudeColor; - QColor majorTickColor; - QColor minorTickColor; - - int meterThickness; - qreal meterFontScaling; - - qreal minimumLevel; - qreal warningLevel; - qreal errorLevel; - qreal clipLevel; - qreal minimumInputLevel; - qreal peakDecayRate; - qreal magnitudeIntegrationTime; - qreal peakHoldDuration; - qreal inputPeakHoldDuration; - - QColor p_backgroundNominalColor; - QColor p_backgroundWarningColor; - QColor p_backgroundErrorColor; - QColor p_foregroundNominalColor; - QColor p_foregroundWarningColor; - QColor p_foregroundErrorColor; - - uint64_t lastRedrawTime = 0; - int channels = 0; - bool clipping = false; - bool vertical; - bool muted = false; - -public: - explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); - ~VolumeMeter(); - - void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]); - QRect getBarRect() const; - bool needLayoutChange(); - - QColor getBackgroundNominalColor() const; - void setBackgroundNominalColor(QColor c); - QColor getBackgroundWarningColor() const; - void setBackgroundWarningColor(QColor c); - QColor getBackgroundErrorColor() const; - void setBackgroundErrorColor(QColor c); - QColor getForegroundNominalColor() const; - void setForegroundNominalColor(QColor c); - QColor getForegroundWarningColor() const; - void setForegroundWarningColor(QColor c); - QColor getForegroundErrorColor() const; - void setForegroundErrorColor(QColor c); - - QColor getBackgroundNominalColorDisabled() const; - void setBackgroundNominalColorDisabled(QColor c); - QColor getBackgroundWarningColorDisabled() const; - void setBackgroundWarningColorDisabled(QColor c); - QColor getBackgroundErrorColorDisabled() const; - void setBackgroundErrorColorDisabled(QColor c); - QColor getForegroundNominalColorDisabled() const; - void setForegroundNominalColorDisabled(QColor c); - QColor getForegroundWarningColorDisabled() const; - void setForegroundWarningColorDisabled(QColor c); - QColor getForegroundErrorColorDisabled() const; - void setForegroundErrorColorDisabled(QColor c); - - QColor getClipColor() const; - void setClipColor(QColor c); - QColor getMagnitudeColor() const; - void setMagnitudeColor(QColor c); - QColor getMajorTickColor() const; - void setMajorTickColor(QColor c); - QColor getMinorTickColor() const; - void setMinorTickColor(QColor c); - int getMeterThickness() const; - void setMeterThickness(int v); - qreal getMeterFontScaling() const; - void setMeterFontScaling(qreal v); - qreal getMinimumLevel() const; - void setMinimumLevel(qreal v); - qreal getWarningLevel() const; - void setWarningLevel(qreal v); - qreal getErrorLevel() const; - void setErrorLevel(qreal v); - qreal getClipLevel() const; - void setClipLevel(qreal v); - qreal getMinimumInputLevel() const; - void setMinimumInputLevel(qreal v); - qreal getPeakDecayRate() const; - void setPeakDecayRate(qreal v); - qreal getMagnitudeIntegrationTime() const; - void setMagnitudeIntegrationTime(qreal v); - qreal getPeakHoldDuration() const; - void setPeakHoldDuration(qreal v); - qreal getInputPeakHoldDuration() const; - void setInputPeakHoldDuration(qreal v); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void wheelEvent(QWheelEvent *event) override; - -protected: - void paintEvent(QPaintEvent *event) override; - void changeEvent(QEvent *e) override; -}; - -class VolumeMeterTimer : public QTimer { - Q_OBJECT - -public: - inline VolumeMeterTimer() : QTimer() {} - - void AddVolControl(VolumeMeter *meter); - void RemoveVolControl(VolumeMeter *meter); - -protected: - void timerEvent(QTimerEvent *event) override; - QList volumeMeters; -}; - -class QLabel; -class VolumeSlider; -class MuteCheckBox; -class OBSSourceLabel; - -class VolControl : public QFrame { - Q_OBJECT - -private: - OBSSource source; - std::vector sigs; - OBSSourceLabel *nameLabel; - QLabel *volLabel; - VolumeMeter *volMeter; - VolumeSlider *slider; - MuteCheckBox *mute; - QPushButton *config = nullptr; - float levelTotal; - float levelCount; - OBSFader obs_fader; - OBSVolMeter obs_volmeter; - bool vertical; - QMenu *contextMenu; - - static void OBSVolumeChanged(void *param, float db); - static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); - static void OBSVolumeMuted(void *data, calldata_t *calldata); - static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); - - void EmitConfigClicked(); - -private slots: - void VolumeChanged(); - void VolumeMuted(bool muted); - void MixersOrMonitoringChanged(); - - void SetMuted(bool checked); - void SliderChanged(int vol); - void updateText(); - -signals: - void ConfigClicked(); - -public: - explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); - ~VolControl(); - - inline obs_source_t *GetSource() const { return source; } - - void SetMeterDecayRate(qreal q); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - - void EnableSlider(bool enable); - inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } - - void refreshColors(); -}; - -class VolumeSlider : public AbsoluteSlider { - Q_OBJECT - -public: - obs_fader_t *fad; - - VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); - VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); - - bool getDisplayTicks() const; - void setDisplayTicks(bool display); - -private: - bool displayTicks = false; - QColor tickColor; - -protected: - virtual void paintEvent(QPaintEvent *event) override; -}; - class VolumeAccessibleInterface : public QAccessibleWidget { public: diff --git a/frontend/widgets/VolumeMeter.cpp b/frontend/widgets/VolumeMeter.cpp index cd00c14d7..5379a9e8d 100644 --- a/frontend/widgets/VolumeMeter.cpp +++ b/frontend/widgets/VolumeMeter.cpp @@ -1,21 +1,13 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolumeMeter.hpp" -#include -#include -#include -#include -#include -#include +#include +#include + +#include +#include #include -using namespace std; - -#define FADER_PRECISION 4096.0 +#include "moc_VolumeMeter.cpp" // Size of the audio indicator in pixels #define INDICATOR_THICKNESS 3 @@ -25,385 +17,6 @@ using namespace std; std::weak_ptr VolumeMeter::updateTimer; -static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) -{ - if (muted) - return Qt::Checked; - else if (unassigned) - return Qt::PartiallyChecked; - else - return Qt::Unchecked; -} - -static inline bool IsSourceUnassigned(obs_source_t *source) -{ - uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); - obs_monitoring_type mt = obs_source_get_monitoring_type(source); - - return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; -} - -static void ShowUnassignedWarning(const char *name) -{ - auto msgBox = [=]() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); - msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); -} - -void VolControl::OBSVolumeChanged(void *data, float db) -{ - Q_UNUSED(db); - VolControl *volControl = static_cast(data); - - QMetaObject::invokeMethod(volControl, "VolumeChanged"); -} - -void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - VolControl *volControl = static_cast(data); - - volControl->volMeter->setLevels(magnitude, peak, inputPeak); -} - -void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) -{ - VolControl *volControl = static_cast(data); - bool muted = calldata_bool(calldata, "muted"); - - QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); -} - -void VolControl::VolumeChanged() -{ - slider->blockSignals(true); - slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); - slider->blockSignals(false); - - updateText(); -} - -void VolControl::VolumeMuted(bool muted) -{ - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) -{ - - VolControl *volControl = static_cast(data); - QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); -} - -void VolControl::MixersOrMonitoringChanged() -{ - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::SetMuted(bool) -{ - bool checked = mute->checkState() == Qt::Checked; - bool prev = obs_source_muted(source); - obs_source_set_muted(source, checked); - bool unassigned = IsSourceUnassigned(source); - - if (!checked && unassigned) { - mute->setCheckState(Qt::PartiallyChecked); - /* Show notice about the source no being assigned to any tracks */ - bool has_shown_warning = - config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); - if (!has_shown_warning) - ShowUnassignedWarning(obs_source_get_name(source)); - } - - auto undo_redo = [](const std::string &uuid, bool val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_muted(source, val); - }; - - QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); - - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); -} - -void VolControl::SliderChanged(int vol) -{ - float prev = obs_source_get_volume(source); - - obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); - updateText(); - - auto undo_redo = [](const std::string &uuid, float val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_volume(source, val); - }; - - float val = obs_source_get_volume(source); - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), - std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); -} - -void VolControl::updateText() -{ - QString text; - float db = obs_fader_get_db(obs_fader); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - volLabel->setText(text); - - bool muted = obs_source_muted(source); - const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; - - QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName); - - slider->setAccessibleName(accText); -} - -void VolControl::EmitConfigClicked() -{ - emit ConfigClicked(); -} - -void VolControl::SetMeterDecayRate(qreal q) -{ - volMeter->setPeakDecayRate(q); -} - -void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - volMeter->setPeakMeterType(peakMeterType); -} - -VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) - : source(std::move(source_)), - levelTotal(0.0f), - levelCount(0.0f), - obs_fader(obs_fader_create(OBS_FADER_LOG)), - obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), - vertical(vertical), - contextMenu(nullptr) -{ - nameLabel = new OBSSourceLabel(source); - volLabel = new QLabel(); - mute = new MuteCheckBox(); - - volLabel->setObjectName("volLabel"); - volLabel->setAlignment(Qt::AlignCenter); - -#ifdef __APPLE__ - mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - QString sourceName = obs_source_get_name(source); - setObjectName(sourceName); - - if (showConfig) { - config = new QPushButton(this); - config->setProperty("class", "icon-dots-vert"); - config->setAutoDefault(false); - - config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); - - connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - } - - QVBoxLayout *mainLayout = new QVBoxLayout; - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - if (vertical) { - QHBoxLayout *nameLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QHBoxLayout *volLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QHBoxLayout *meterLayout = new QHBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new VolumeSlider(obs_fader, Qt::Vertical); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - nameLayout->setAlignment(Qt::AlignCenter); - meterLayout->setAlignment(Qt::AlignCenter); - controlLayout->setAlignment(Qt::AlignCenter); - volLayout->setAlignment(Qt::AlignCenter); - - meterFrame->setObjectName("volMeterFrame"); - - nameLayout->setContentsMargins(0, 0, 0, 0); - nameLayout->setSpacing(0); - nameLayout->addWidget(nameLabel); - - controlLayout->setContentsMargins(0, 0, 0, 0); - controlLayout->setSpacing(0); - - // Add Headphone (audio monitoring) widget here - controlLayout->addWidget(mute); - - if (showConfig) { - controlLayout->addWidget(config); - } - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - meterLayout->addWidget(slider); - meterLayout->addWidget(volMeter); - - meterFrame->setLayout(meterLayout); - - volLayout->setContentsMargins(0, 0, 0, 0); - volLayout->setSpacing(0); - volLayout->addWidget(volLabel); - volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); - - mainLayout->addItem(nameLayout); - mainLayout->addItem(volLayout); - mainLayout->addWidget(meterFrame); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - - // Default size can cause clipping of long names in vertical layout. - QFont font = nameLabel->font(); - QFontInfo info(font); - nameLabel->setFont(font); - - setMaximumWidth(110); - } else { - QHBoxLayout *textLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QVBoxLayout *meterLayout = new QVBoxLayout; - QVBoxLayout *buttonLayout = new QVBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - slider = new VolumeSlider(obs_fader, Qt::Horizontal); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - meterFrame->setObjectName("volMeterFrame"); - meterFrame->setLayout(meterLayout); - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - - meterLayout->addWidget(volMeter); - meterLayout->addWidget(slider); - - buttonLayout->setContentsMargins(0, 0, 0, 0); - buttonLayout->setSpacing(0); - - if (showConfig) { - buttonLayout->addWidget(config); - } - buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); - buttonLayout->addWidget(mute); - - controlLayout->addItem(buttonLayout); - controlLayout->addWidget(meterFrame); - - mainLayout->addItem(textLayout); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - } - - setLayout(mainLayout); - - nameLabel->setText(sourceName); - - slider->setMinimum(0); - slider->setMaximum(int(FADER_PRECISION)); - - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - mute->setCheckState(GetCheckState(muted, unassigned)); - volMeter->muted = muted || unassigned; - mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); - obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, - this); - - QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); - QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); - - obs_fader_attach_source(obs_fader, source); - obs_volmeter_attach_source(obs_volmeter, source); - - /* Call volume changed once to init the slider position and label */ - VolumeChanged(); -} - -void VolControl::EnableSlider(bool enable) -{ - slider->setEnabled(enable); -} - -VolControl::~VolControl() -{ - obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.clear(); - - if (contextMenu) - contextMenu->close(); -} - static inline QColor color_from_int(long long val) { QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); @@ -636,16 +249,6 @@ void VolumeMeter::setMeterFontScaling(qreal v) recalculateLayout = true; } -void VolControl::refreshColors() -{ - volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); - volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); - volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); - volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); - volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); - volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); -} - qreal VolumeMeter::getMinimumLevel() const { return minimumLevel; @@ -1349,159 +952,3 @@ void VolumeMeter::changeEvent(QEvent *e) QWidget::changeEvent(e); } - -void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) -{ - volumeMeters.push_back(meter); -} - -void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) -{ - volumeMeters.removeOne(meter); -} - -void VolumeMeterTimer::timerEvent(QTimerEvent *) -{ - for (VolumeMeter *meter : volumeMeters) { - if (meter->needLayoutChange()) { - // Tell paintEvent to update layout and paint everything - meter->update(); - } else { - // Tell paintEvent to paint only the bars - meter->update(meter->getBarRect()); - } - } -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) -{ - fad = fader; -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) - : AbsoluteSlider(orientation, parent) -{ - fad = fader; -} - -bool VolumeSlider::getDisplayTicks() const -{ - return displayTicks; -} - -void VolumeSlider::setDisplayTicks(bool display) -{ - displayTicks = display; -} - -void VolumeSlider::paintEvent(QPaintEvent *event) -{ - if (!getDisplayTicks()) { - QSlider::paintEvent(event); - return; - } - - QPainter painter(this); - QColor tickColor(91, 98, 115, 255); - - obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); - - QStyleOptionSlider opt; - initStyleOption(&opt); - - QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - if (orientation() == Qt::Horizontal) { - const int sliderWidth = groove.width() - handle.width(); - - float tickLength = groove.height() * 1.5; - tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); - - float yPos = groove.center().y() - (tickLength / 2) + 1; - - for (int db = -10; db >= -90; db -= 10) { - float tickValue = fader_db_to_def(db); - - float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); - painter.fillRect(xPos, yPos, 1, tickLength, tickColor); - } - } - - if (orientation() == Qt::Vertical) { - const int sliderHeight = groove.height() - handle.height(); - - float tickLength = groove.width() * 1.5; - tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); - - float xPos = groove.center().x() - (tickLength / 2) + 1; - - for (int db = -10; db >= -96; db -= 10) { - float tickValue = fader_db_to_def(db); - - float yPos = - groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); - painter.fillRect(xPos, yPos, tickLength, 1, tickColor); - } - } - - QSlider::paintEvent(event); -} - -VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} - -VolumeSlider *VolumeAccessibleInterface::slider() const -{ - return qobject_cast(object()); -} - -QString VolumeAccessibleInterface::text(QAccessible::Text t) const -{ - if (slider()->isVisible()) { - switch (t) { - case QAccessible::Text::Value: - return currentValue().toString(); - default: - break; - } - } - return QAccessibleWidget::text(t); -} - -QVariant VolumeAccessibleInterface::currentValue() const -{ - QString text; - float db = obs_fader_get_db(slider()->fad); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - return text; -} - -void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) -{ - slider()->setValue(value.toInt()); -} - -QVariant VolumeAccessibleInterface::maximumValue() const -{ - return slider()->maximum(); -} - -QVariant VolumeAccessibleInterface::minimumValue() const -{ - return slider()->minimum(); -} - -QVariant VolumeAccessibleInterface::minimumStepSize() const -{ - return slider()->singleStep(); -} - -QAccessible::Role VolumeAccessibleInterface::role() const -{ - return QAccessible::Role::Slider; -} diff --git a/frontend/widgets/VolumeMeter.hpp b/frontend/widgets/VolumeMeter.hpp index 51c77f247..a8cbc3d45 100644 --- a/frontend/widgets/VolumeMeter.hpp +++ b/frontend/widgets/VolumeMeter.hpp @@ -1,18 +1,13 @@ #pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include "absolute-slider.hpp" -class QPushButton; +#include +#include + +#define FADER_PRECISION 4096.0 + class VolumeMeterTimer; -class VolumeSlider; class VolumeMeter : public QWidget { Q_OBJECT @@ -225,117 +220,3 @@ protected: void paintEvent(QPaintEvent *event) override; void changeEvent(QEvent *e) override; }; - -class VolumeMeterTimer : public QTimer { - Q_OBJECT - -public: - inline VolumeMeterTimer() : QTimer() {} - - void AddVolControl(VolumeMeter *meter); - void RemoveVolControl(VolumeMeter *meter); - -protected: - void timerEvent(QTimerEvent *event) override; - QList volumeMeters; -}; - -class QLabel; -class VolumeSlider; -class MuteCheckBox; -class OBSSourceLabel; - -class VolControl : public QFrame { - Q_OBJECT - -private: - OBSSource source; - std::vector sigs; - OBSSourceLabel *nameLabel; - QLabel *volLabel; - VolumeMeter *volMeter; - VolumeSlider *slider; - MuteCheckBox *mute; - QPushButton *config = nullptr; - float levelTotal; - float levelCount; - OBSFader obs_fader; - OBSVolMeter obs_volmeter; - bool vertical; - QMenu *contextMenu; - - static void OBSVolumeChanged(void *param, float db); - static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); - static void OBSVolumeMuted(void *data, calldata_t *calldata); - static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); - - void EmitConfigClicked(); - -private slots: - void VolumeChanged(); - void VolumeMuted(bool muted); - void MixersOrMonitoringChanged(); - - void SetMuted(bool checked); - void SliderChanged(int vol); - void updateText(); - -signals: - void ConfigClicked(); - -public: - explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); - ~VolControl(); - - inline obs_source_t *GetSource() const { return source; } - - void SetMeterDecayRate(qreal q); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - - void EnableSlider(bool enable); - inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } - - void refreshColors(); -}; - -class VolumeSlider : public AbsoluteSlider { - Q_OBJECT - -public: - obs_fader_t *fad; - - VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); - VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); - - bool getDisplayTicks() const; - void setDisplayTicks(bool display); - -private: - bool displayTicks = false; - QColor tickColor; - -protected: - virtual void paintEvent(QPaintEvent *event) override; -}; - -class VolumeAccessibleInterface : public QAccessibleWidget { - -public: - VolumeAccessibleInterface(QWidget *w); - - QVariant currentValue() const; - void setCurrentValue(const QVariant &value); - - QVariant maximumValue() const; - QVariant minimumValue() const; - - QVariant minimumStepSize() const; - -private: - VolumeSlider *slider() const; - -protected: - virtual QAccessible::Role role() const override; - virtual QString text(QAccessible::Text t) const override; -}; From c792be35a12da2205dd2d7359fe17b09b0e966c4 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 19:07:10 +0100 Subject: [PATCH 24/37] frontend: Prepare Qt UI autoconfig wizards for splits --- .../wizards/AutoConfig.cpp | 0 .../wizards/AutoConfig.hpp | 0 frontend/wizards/AutoConfigStartPage.cpp | 1240 ++++++++++++++++ frontend/wizards/AutoConfigStartPage.hpp | 288 ++++ frontend/wizards/AutoConfigStreamPage.cpp | 1240 ++++++++++++++++ frontend/wizards/AutoConfigStreamPage.hpp | 288 ++++ .../wizards/AutoConfigTestPage.cpp | 0 frontend/wizards/AutoConfigTestPage.hpp | 78 + frontend/wizards/AutoConfigVideoPage.cpp | 1240 ++++++++++++++++ frontend/wizards/AutoConfigVideoPage.hpp | 288 ++++ frontend/wizards/TestMode.hpp | 1261 +++++++++++++++++ 11 files changed, 5923 insertions(+) rename UI/window-basic-auto-config.cpp => frontend/wizards/AutoConfig.cpp (100%) rename UI/window-basic-auto-config.hpp => frontend/wizards/AutoConfig.hpp (100%) create mode 100644 frontend/wizards/AutoConfigStartPage.cpp create mode 100644 frontend/wizards/AutoConfigStartPage.hpp create mode 100644 frontend/wizards/AutoConfigStreamPage.cpp create mode 100644 frontend/wizards/AutoConfigStreamPage.hpp rename UI/window-basic-auto-config-test.cpp => frontend/wizards/AutoConfigTestPage.cpp (100%) create mode 100644 frontend/wizards/AutoConfigTestPage.hpp create mode 100644 frontend/wizards/AutoConfigVideoPage.cpp create mode 100644 frontend/wizards/AutoConfigVideoPage.hpp create mode 100644 frontend/wizards/TestMode.hpp diff --git a/UI/window-basic-auto-config.cpp b/frontend/wizards/AutoConfig.cpp similarity index 100% rename from UI/window-basic-auto-config.cpp rename to frontend/wizards/AutoConfig.cpp diff --git a/UI/window-basic-auto-config.hpp b/frontend/wizards/AutoConfig.hpp similarity index 100% rename from UI/window-basic-auto-config.hpp rename to frontend/wizards/AutoConfig.hpp diff --git a/frontend/wizards/AutoConfigStartPage.cpp b/frontend/wizards/AutoConfigStartPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#include "moc_window-basic-auto-config.cpp" +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "url-push-button.hpp" + +#include "goliveapi-postdata.hpp" +#include "goliveapi-network.hpp" +#include "multitrack-video-error.hpp" + +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigVideoPage.h" +#include "ui_AutoConfigStreamPage.h" + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "auth-oauth.hpp" +#include "ui-config.h" +#ifdef YOUTUBE_ENABLED +#include "youtube-api-wrappers.hpp" +#endif + +struct QCef; +struct QCefCookieManager; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +#define wiz reinterpret_cast(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + if (!std::filesystem::exists(jsonFilePath)) { + return OBSData(); + } + + OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + return settings.Get(); +} + +static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) +{ + OBSData settings = OpenServiceSettings(type); + + service = obs_data_get_string(settings, "service"); + server = obs_data_get_string(settings, "server"); + key = obs_data_get_string(settings, "key"); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.StartPage")); + setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); + + OBSBasic *main = OBSBasic::Get(); + if (main->VCamEnabled()) { + QRadioButton *prioritizeVCam = + new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); + QBoxLayout *box = reinterpret_cast(layout()); + box->insertWidget(2, prioritizeVCam); + + connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); + } +} + +AutoConfigStartPage::~AutoConfigStartPage() {} + +int AutoConfigStartPage::nextId() const +{ + return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; +} + +void AutoConfigStartPage::on_prioritizeStreaming_clicked() +{ + wiz->type = AutoConfig::Type::Streaming; +} + +void AutoConfigStartPage::on_prioritizeRecording_clicked() +{ + wiz->type = AutoConfig::Type::Recording; +} + +void AutoConfigStartPage::PrioritizeVCam() +{ + wiz->type = AutoConfig::Type::VirtualCam; +} + +/* ------------------------------------------------------------------------- */ + +#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x +#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") +#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") +#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") +#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") +#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") + +AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) +{ + ui->setupUi(this); + + setTitle(QTStr("Basic.AutoConfig.VideoPage")); + setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; + + QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); + + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); + ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); + ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); + ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); + ui->fps->setCurrentIndex(0); + + QString cxStr = QString::number(ovi.base_width); + QString cyStr = QString::number(ovi.base_height); + + int encRes = int(ovi.base_width << 16) | int(ovi.base_height); + + // Auto config only supports testing down to 240p, don't allow current + // resolution if it's lower than that. + if (ovi.base_height >= 240) + ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); + + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QSize as = screen->size(); + int as_width = as.width(); + int as_height = as.height(); + + // Calculate physical screen resolution based on the virtual screen resolution + // They might differ if scaling is enabled, e.g. for HiDPI screens + as_width = round(as_width * screen->devicePixelRatio()); + as_height = round(as_height * screen->devicePixelRatio()); + + encRes = as_width << 16 | as_height; + + QString str = + QTStr(RES_USE_DISPLAY) + .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); + + ui->canvasRes->addItem(str, encRes); + } + + auto addRes = [&](int cx, int cy) { + encRes = (cx << 16) | cy; + QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); + ui->canvasRes->addItem(str, encRes); + }; + + addRes(1920, 1080); + addRes(1280, 720); + + ui->canvasRes->setCurrentIndex(0); +} + +AutoConfigVideoPage::~AutoConfigVideoPage() {} + +int AutoConfigVideoPage::nextId() const +{ + return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; +} + +bool AutoConfigVideoPage::validatePage() +{ + int encRes = ui->canvasRes->currentData().toInt(); + wiz->baseResolutionCX = encRes >> 16; + wiz->baseResolutionCY = encRes & 0xFFFF; + wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + switch (wiz->fpsType) { + case AutoConfig::FPSType::PreferHighFPS: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = true; + break; + case AutoConfig::FPSType::PreferHighRes: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::UseCurrent: + wiz->specificFPSNum = ovi.fps_num; + wiz->specificFPSDen = ovi.fps_den; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps30: + wiz->specificFPSNum = 30; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps60: + wiz->specificFPSNum = 60; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + } + + return true; +} + +/* ------------------------------------------------------------------------- */ + +enum class ListOpt : int { + ShowAll = 1, + Custom, +}; + +AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) +{ + ui->setupUi(this); + ui->bitrateLabel->setVisible(false); + ui->bitrate->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(false); + ui->useMultitrackVideo->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + int vertSpacing = ui->topLayout->verticalSpacing(); + + QMargins m = ui->topLayout->contentsMargins(); + m.setBottom(vertSpacing / 2); + ui->topLayout->setContentsMargins(m); + + m = ui->loginPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->loginPageLayout->setContentsMargins(m); + + m = ui->streamkeyPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->streamkeyPageLayout->setContentsMargins(m); + + setTitle(QTStr("Basic.AutoConfig.StreamPage")); + setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); + + LoadServices(false); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); + + connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + }); + + connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); +} + +AutoConfigStreamPage::~AutoConfigStreamPage() {} + +bool AutoConfigStreamPage::isComplete() const +{ + return ready; +} + +int AutoConfigStreamPage::nextId() const +{ + return AutoConfig::TestPage; +} + +inline bool AutoConfigStreamPage::IsCustomService() const +{ + return ui->service->currentData().toInt() == (int)ListOpt::Custom; +} + +bool AutoConfigStreamPage::validatePage() +{ + OBSDataAutoRelease service_settings = obs_data_create(); + + wiz->customServer = IsCustomService(); + + const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; + + if (!wiz->customServer) { + obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); + } + + OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); + + int bitrate; + if (!ui->doBandwidthTest->isChecked()) { + bitrate = ui->bitrate->value(); + wiz->idealBitrate = bitrate; + } else { + /* Default test target is 10 Mbps */ + bitrate = 10000; +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(wiz->serviceName)) { + /* Adjust upper bound to YouTube limits + * for resolutions above 1080p */ + if (wiz->baseResolutionCY > 1440) + bitrate = 51000; + else if (wiz->baseResolutionCY > 1080) + bitrate = 18000; + } +#endif + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", bitrate); + obs_service_apply_encoder_settings(service, settings, nullptr); + + if (wiz->customServer) { + QString server = ui->customServer->text().trimmed(); + wiz->server = wiz->serverName = QT_TO_UTF8(server); + } else { + wiz->serverName = QT_TO_UTF8(ui->server->currentText()); + wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); + } + + wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); + wiz->idealBitrate = wiz->startingBitrate; + wiz->regionUS = ui->regionUS->isChecked(); + wiz->regionEU = ui->regionEU->isChecked(); + wiz->regionAsia = ui->regionAsia->isChecked(); + wiz->regionOther = ui->regionOther->isChecked(); + wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); + if (ui->preferHardware) + wiz->preferHardware = ui->preferHardware->isChecked(); + wiz->key = QT_TO_UTF8(ui->key->text()); + + if (!wiz->customServer) { + if (wiz->serviceName == "Twitch") + wiz->service = AutoConfig::Service::Twitch; +#ifdef YOUTUBE_ENABLED + else if (IsYouTubeService(wiz->serviceName)) + wiz->service = AutoConfig::Service::YouTube; +#endif + else if (wiz->serviceName == "Amazon IVS") + wiz->service = AutoConfig::Service::AmazonIVS; + else + wiz->service = AutoConfig::Service::Other; + } else { + wiz->service = AutoConfig::Service::Other; + } + + if (wiz->service == AutoConfig::Service::Twitch) { + wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); + + if (wiz->testMultitrackVideo) { + auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, + std::nullopt, false); + + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + try { + auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), + postData, multitrack_video_name); + + for (const auto &endpoint : config.ingest_endpoints) { + if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) + continue; + + std::string address = endpoint.url_template; + auto pos = address.find("/{stream_key}"); + if (pos != address.npos) + address.erase(pos); + + wiz->serviceConfigServers.push_back({address, address}); + } + + int multitrackVideoBitrate = 0; + for (auto &encoder_config : config.encoder_configurations) { + auto it = encoder_config.settings.find("bitrate"); + if (it == encoder_config.settings.end()) + continue; + + if (!it->is_number_integer()) + continue; + + int bitrate = 0; + it->get_to(bitrate); + multitrackVideoBitrate += bitrate; + } + + if (multitrackVideoBitrate > 0) { + wiz->startingBitrate = multitrackVideoBitrate; + wiz->idealBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.testSuccessful = true; + } + } catch (const MultitrackVideoError & /*err*/) { + // FIXME: do something sensible + } + } + } + + if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && + wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { + QMessageBox::StandardButton button; +#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) + button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); +#undef WARNING_TEXT + + if (button == QMessageBox::No) + return false; + } + + return true; +} + +void AutoConfigStreamPage::on_show_clicked() +{ + if (ui->key->echoMode() == QLineEdit::Password) { + ui->key->setEchoMode(QLineEdit::Normal); + ui->show->setText(QTStr("Hide")); + } else { + ui->key->setEchoMode(QLineEdit::Password); + ui->show->setText(QTStr("Show")); + } +} + +void AutoConfigStreamPage::OnOAuthStreamKeyConnected() +{ + OAuthStreamKey *a = reinterpret_cast(auth.get()); + + if (a) { + bool validKey = !a->key().empty(); + + if (validKey) + ui->key->setText(QT_UTF8(a->key().c_str())); + + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(a->service())) { + ui->key->clear(); + + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + + ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); + + YoutubeApiWrappers *ytAuth = reinterpret_cast(a); + ChannelDescription cd; + if (ytAuth->GetChannelDescription(cd)) { + ui->connectedAccountText->setText(cd.title); + + /* Create throwaway stream key for bandwidth test */ + if (ui->doBandwidthTest->isChecked()) { + StreamDescription stream = {"", "", "OBS Studio Test Stream"}; + if (ytAuth->InsertStream(stream)) { + ui->key->setText(stream.name); + } + } + } + } +#endif + } + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::OnAuthConnected() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + Auth::Type type = Auth::AuthType(service); + + if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { + OnOAuthStreamKeyConnected(); + } +} + +void AutoConfigStreamPage::on_connectAccount_clicked() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + + OAuth::DeleteCookies(service); + + auth = OAuthStreamKey::Login(this, service); + if (!!auth) { + OnAuthConnected(); + + ui->useStreamKeyAdv->setVisible(false); + } +} + +#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" +#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" + +void AutoConfigStreamPage::on_disconnectAccount_clicked() +{ + QMessageBox::StandardButton button; + + button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); + + if (button == QMessageBox::No) { + return; + } + + OBSBasic *main = OBSBasic::Get(); + + main->auth.reset(); + auth.reset(); + + std::string service = QT_TO_UTF8(ui->service->currentText()); + +#ifdef BROWSER_AVAILABLE + OAuth::DeleteCookies(service); +#endif + + reset_service_ui_fields(service); + + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->key->setText(""); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + /* Restore key link when disconnecting account */ + UpdateKeyLink(); +} + +void AutoConfigStreamPage::on_useStreamKey_clicked() +{ + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::on_preferHardware_clicked() +{ + auto *main = OBSBasic::Get(); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + + ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); + ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); + ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); +} + +static inline bool is_auth_service(const std::string &service) +{ + return Auth::AuthType(service) != Auth::Type::None; +} + +static inline bool is_external_oauth(const std::string &service) +{ + return Auth::External(service); +} + +void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) +{ +#ifdef YOUTUBE_ENABLED + // when account is already connected: + OAuthStreamKey *a = reinterpret_cast(auth.get()); + if (a && service == a->service() && IsYouTubeService(a->service())) { + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + return; + } +#endif + + bool external_oauth = is_external_oauth(service); + if (external_oauth) { + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(true); + ui->useStreamKeyAdv->setVisible(true); + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + + } else if (cef) { + QString key = ui->key->text(); + bool can_auth = is_auth_service(service); + int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; + + ui->stackedWidget->setCurrentIndex(page); + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->connectAccount2->setVisible(can_auth); + ui->useStreamKeyAdv->setVisible(false); + } else { + ui->connectAccount2->setVisible(false); + ui->useStreamKeyAdv->setVisible(false); + } + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + ui->disconnectAccount->setVisible(false); +} + +void AutoConfigStreamPage::ServiceChanged() +{ + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + if (showMore) + return; + + std::string service = QT_TO_UTF8(ui->service->currentText()); + bool regionBased = service == "Twitch"; + bool testBandwidth = ui->doBandwidthTest->isChecked(); + bool custom = IsCustomService(); + + bool ertmp_multitrack_video_available = service == "Twitch"; + + bool custom_disclaimer = false; + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (!custom) { + OBSDataAutoRelease service_settings = obs_data_create(); + obs_data_set_string(service_settings, "service", service.c_str()); + OBSServiceAutoRelease obs_service = + obs_service_create("rtmp_common", "temp service", service_settings, nullptr); + + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setText( + obs_data_get_string(service_settings, "multitrack_video_disclaimer")); + custom_disclaimer = true; + } + } + + if (!custom_disclaimer) { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); + } + + ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setText( + QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); + ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); + ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); + + reset_service_ui_fields(service); + + /* Test three closest servers if "Auto" is available for Twitch */ + if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) + regionBased = false; + + ui->streamkeyPageLayout->removeWidget(ui->serverLabel); + ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); + + if (custom) { + ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(false); + ui->serverStackedWidget->setCurrentIndex(1); + ui->serverStackedWidget->setVisible(true); + ui->serverLabel->setVisible(true); + } else { + if (!testBandwidth) + ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(regionBased && testBandwidth); + ui->serverStackedWidget->setCurrentIndex(0); + ui->serverStackedWidget->setHidden(testBandwidth); + ui->serverLabel->setHidden(testBandwidth); + } + + wiz->testRegions = regionBased && testBandwidth; + + ui->bitrateLabel->setHidden(testBandwidth); + ui->bitrate->setHidden(testBandwidth); + + OBSBasic *main = OBSBasic::Get(); + + if (main->auth) { + auto system_auth_service = main->auth->service(); + bool service_check = service.find(system_auth_service) != std::string::npos; +#ifdef YOUTUBE_ENABLED + service_check = service_check ? service_check + : IsYouTubeService(system_auth_service) && IsYouTubeService(service); +#endif + if (service_check) { + auth.reset(); + auth = main->auth; + OnAuthConnected(); + } + } + + UpdateCompleted(); +} + +void AutoConfigStreamPage::UpdateMoreInfoLink() +{ + if (IsCustomService()) { + ui->moreInfoButton->hide(); + return; + } + + QString serviceName = ui->service->currentText(); + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + const char *more_info_link = obs_data_get_string(settings, "more_info_link"); + + if (!more_info_link || (*more_info_link == '\0')) { + ui->moreInfoButton->hide(); + } else { + ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); + ui->moreInfoButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateKeyLink() +{ + QString serviceName = ui->service->currentText(); + QString customServer = ui->customServer->text().trimmed(); + QString streamKeyLink; + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + + if (customServer.contains("fbcdn.net") && IsCustomService()) { + streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; + } + + if (serviceName == "Dacast") { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); + ui->streamKeyLabel->setToolTip(""); + } else if (!IsCustomService()) { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); + ui->streamKeyLabel->setToolTip(""); + } else { + /* add tooltips for stream key */ + QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; + QString lStr = "%1 "; + + ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); + ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); + } + + if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { + ui->streamKeyButton->hide(); + } else { + ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); + ui->streamKeyButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::LoadServices(bool showAll) +{ + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_bool(settings, "show_all", showAll); + + obs_property_t *prop = obs_properties_get(props, "show_all"); + obs_property_modified(prop, settings); + + ui->service->blockSignals(true); + ui->service->clear(); + + QStringList names; + + obs_property_t *services = obs_properties_get(props, "service"); + size_t services_count = obs_property_list_item_count(services); + for (size_t i = 0; i < services_count; i++) { + const char *name = obs_property_list_item_string(services, i); + names.push_back(name); + } + + if (showAll) + names.sort(Qt::CaseInsensitive); + + for (QString &name : names) + ui->service->addItem(name); + + if (!showAll) { + ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), + QVariant((int)ListOpt::ShowAll)); + } + + ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); + + if (!lastService.isEmpty()) { + int idx = ui->service->findText(lastService); + if (idx != -1) + ui->service->setCurrentIndex(idx); + } + + obs_properties_destroy(props); + + ui->service->blockSignals(false); +} + +void AutoConfigStreamPage::UpdateServerList() +{ + QString serviceName = ui->service->currentText(); + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + + if (showMore) { + LoadServices(true); + ui->service->showPopup(); + return; + } else { + lastService = serviceName; + } + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + obs_property_t *servers = obs_properties_get(props, "server"); + + ui->server->clear(); + + size_t servers_count = obs_property_list_item_count(servers); + for (size_t i = 0; i < servers_count; i++) { + const char *name = obs_property_list_item_name(servers, i); + const char *server = obs_property_list_item_string(servers, i); + ui->server->addItem(name, server); + } + + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateCompleted() +{ + const bool custom = IsCustomService(); + if (ui->stackedWidget->currentIndex() == (int)Section::Connect || + (ui->key->text().isEmpty() && !auth && !custom)) { + ready = false; + } else { + if (custom) { + ready = !ui->customServer->text().isEmpty(); + } else { + ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || + ui->regionAsia->isChecked() || ui->regionOther->isChecked(); + } + } + emit completeChanged(); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) +{ + EnableThreadedMessageBoxes(true); + + calldata_t cd = {0}; + calldata_set_int(&cd, "seconds", 5); + + proc_handler_t *ph = obs_get_proc_handler(); + proc_handler_call(ph, "twitch_ingests_refresh", &cd); + proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); + calldata_free(&cd); + + OBSBasic *main = reinterpret_cast(parent); + main->EnableOutputs(false); + + installEventFilter(CreateShortcutFilter()); + + std::string serviceType; + GetServiceInfo(serviceType, serviceName, server, key); +#if defined(_WIN32) || defined(__APPLE__) + setWizardStyle(QWizard::ModernStyle); +#endif + streamPage = new AutoConfigStreamPage(); + + setPage(StartPage, new AutoConfigStartPage()); + setPage(VideoPage, new AutoConfigVideoPage()); + setPage(StreamPage, streamPage); + setPage(TestPage, new AutoConfigTestPage()); + setWindowTitle(QTStr("Basic.AutoConfig")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + baseResolutionCX = ovi.base_width; + baseResolutionCY = ovi.base_height; + + /* ----------------------------------------- */ + /* check to see if Twitch's "auto" available */ + + OBSDataAutoRelease twitchSettings = obs_data_create(); + + obs_data_set_string(twitchSettings, "service", "Twitch"); + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, twitchSettings); + + obs_property_t *p = obs_properties_get(props, "server"); + const char *first = obs_property_list_item_string(p, 0); + twitchAuto = strcmp(first, "auto") == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* check to see if Amazon IVS "auto" entries are available */ + + OBSDataAutoRelease amazonIVSSettings = obs_data_create(); + + obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); + + props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, amazonIVSSettings); + + p = obs_properties_get(props, "server"); + first = obs_property_list_item_string(p, 0); + amazonIVSAuto = strncmp(first, "auto", 4) == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* load service/servers */ + + customServer = serviceType == "rtmp_custom"; + + QComboBox *serviceList = streamPage->ui->service; + + if (!serviceName.empty()) { + serviceList->blockSignals(true); + + int count = serviceList->count(); + bool found = false; + + for (int i = 0; i < count; i++) { + QString name = serviceList->itemText(i); + + if (name == serviceName.c_str()) { + serviceList->setCurrentIndex(i); + found = true; + break; + } + } + + if (!found) { + serviceList->insertItem(0, serviceName.c_str()); + serviceList->setCurrentIndex(0); + } + + serviceList->blockSignals(false); + } + + streamPage->UpdateServerList(); + streamPage->UpdateKeyLink(); + streamPage->UpdateMoreInfoLink(); + streamPage->lastService.clear(); + + if (!customServer) { + QComboBox *serverList = streamPage->ui->server; + int idx = serverList->findData(QString(server.c_str())); + if (idx == -1) + idx = 0; + + serverList->setCurrentIndex(idx); + } else { + streamPage->ui->customServer->setText(server.c_str()); + int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); + streamPage->ui->service->setCurrentIndex(idx); + } + + if (!key.empty()) + streamPage->ui->key->setText(key.c_str()); + + TestHardwareEncoding(); + + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + streamPage->ui->bitrate->setValue(bitrate); + streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); + streamPage->ServiceChanged(); + + if (!hardwareEncodingAvailable) { + delete streamPage->ui->preferHardware; + streamPage->ui->preferHardware = nullptr; + } else { + /* Newer generations of NVENC have a high enough quality to + * bitrate ratio that if NVENC is available, it makes sense to + * just always prefer hardware encoding by default */ + bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; + streamPage->ui->preferHardware->setChecked(preferHardware); + } + + setOptions(QWizard::WizardOptions()); + setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); + setButtonText(QWizard::BackButton, QTStr("Back")); + setButtonText(QWizard::NextButton, QTStr("Next")); + setButtonText(QWizard::CancelButton, QTStr("Cancel")); +} + +AutoConfig::~AutoConfig() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + main->EnableOutputs(true); + EnableThreadedMessageBoxes(false); +} + +void AutoConfig::TestHardwareEncoding() +{ + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) { + if (strcmp(id, "ffmpeg_nvenc") == 0) + hardwareEncodingAvailable = nvencAvailable = true; + else if (strcmp(id, "obs_qsv11") == 0) + hardwareEncodingAvailable = qsvAvailable = true; + else if (strcmp(id, "h264_texture_amf") == 0) + hardwareEncodingAvailable = vceAvailable = true; +#ifdef __APPLE__ + else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + if (__builtin_available(macOS 13.0, *)) + hardwareEncodingAvailable = appleAvailable = true; +#endif + } +} + +bool AutoConfig::CanTestServer(const char *server) +{ + if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) + return true; + + if (service == Service::Twitch) { + if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || + astrcmp_n(server, "US Central:", 11) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else { + return true; + } + + return false; +} + +void AutoConfig::done(int result) +{ + QWizard::done(result); + + if (result == QDialog::Accepted) { + if (type == Type::Streaming) + SaveStreamSettings(); + SaveSettings(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) { + OBSBasic *main = OBSBasic::Get(); + main->NewYouTubeAppDock(); + } +#endif + } +} + +inline const char *AutoConfig::GetEncoderId(Encoder enc) +{ + switch (enc) { + case Encoder::NVENC: + return SIMPLE_ENCODER_NVENC; + case Encoder::QSV: + return SIMPLE_ENCODER_QSV; + case Encoder::AMD: + return SIMPLE_ENCODER_AMD; + case Encoder::Apple: + return SIMPLE_ENCODER_APPLE_H264; + default: + return SIMPLE_ENCODER_X264; + } +}; + +void AutoConfig::SaveStreamSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* ---------------------------------- */ + /* save service */ + + const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + + obs_service_t *oldService = main->GetService(); + OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); + + OBSDataAutoRelease settings = obs_data_create(); + + if (!customServer) + obs_data_set_string(settings, "service", serviceName.c_str()); + obs_data_set_string(settings, "server", server.c_str()); +#ifdef YOUTUBE_ENABLED + if (!streamPage->auth || !IsYouTubeService(serviceName)) + obs_data_set_string(settings, "key", key.c_str()); +#else + obs_data_set_string(settings, "key", key.c_str()); +#endif + + OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); + + if (!newService) + return; + + main->SetService(newService); + main->SaveService(); + main->auth = streamPage->auth; + if (!!main->auth) { + main->auth->LoadUI(); + main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); + } else { + main->SetBroadcastFlowEnabled(false); + } + + /* ---------------------------------- */ + /* save stream settings */ + + config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); + config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); + config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); + + config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); + + if (multitrackVideo.targetBitrate.has_value()) + config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", + *multitrackVideo.targetBitrate); + else + config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); + + if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && + (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + } else if (multitrackVideo.bitrate.has_value()) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); + config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", + *multitrackVideo.bitrate); + } +} + +void AutoConfig::SaveSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (recordingEncoder != Encoder::Stream) + config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); + + const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; + + config_set_string(main->Config(), "Output", "Mode", "Simple"); + config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); + config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); + config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); + config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); + config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); + + if (fpsType != FPSType::UseCurrent) { + config_set_uint(main->Config(), "Video", "FPSType", 0); + config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); + } + + main->ResetVideo(); + main->ResetOutputs(); + config_save_safe(main->Config(), "tmp", nullptr); +} diff --git a/frontend/wizards/AutoConfigStartPage.hpp b/frontend/wizards/AutoConfigStartPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class Ui_AutoConfigStartPage; +class Ui_AutoConfigVideoPage; +class Ui_AutoConfigStreamPage; +class Ui_AutoConfigTestPage; + +class AutoConfigStreamPage; +class Auth; + +class AutoConfig : public QWizard { + Q_OBJECT + + friend class AutoConfigStartPage; + friend class AutoConfigVideoPage; + friend class AutoConfigStreamPage; + friend class AutoConfigTestPage; + + enum class Type { + Invalid, + Streaming, + Recording, + VirtualCam, + }; + + enum class Service { + Twitch, + YouTube, + AmazonIVS, + Other, + }; + + enum class Encoder { + x264, + NVENC, + QSV, + AMD, + Apple, + Stream, + }; + + enum class Quality { + Stream, + High, + }; + + enum class FPSType : int { + PreferHighFPS, + PreferHighRes, + UseCurrent, + fps30, + fps60, + }; + + struct StreamServer { + std::string name; + std::string address; + }; + + static inline const char *GetEncoderId(Encoder enc); + + AutoConfigStreamPage *streamPage = nullptr; + + Service service = Service::Other; + Quality recordingQuality = Quality::Stream; + Encoder recordingEncoder = Encoder::Stream; + Encoder streamingEncoder = Encoder::x264; + Type type = Type::Streaming; + FPSType fpsType = FPSType::PreferHighFPS; + int idealBitrate = 2500; + struct { + std::optional targetBitrate; + std::optional bitrate; + bool testSuccessful = false; + } multitrackVideo; + int baseResolutionCX = 1920; + int baseResolutionCY = 1080; + int idealResolutionCX = 1280; + int idealResolutionCY = 720; + int idealFPSNum = 60; + int idealFPSDen = 1; + std::string serviceName; + std::string serverName; + std::string server; + std::vector serviceConfigServers; + std::string key; + + bool hardwareEncodingAvailable = false; + bool nvencAvailable = false; + bool qsvAvailable = false; + bool vceAvailable = false; + bool appleAvailable = false; + + int startingBitrate = 2500; + bool customServer = false; + bool bandwidthTest = false; + bool testMultitrackVideo = false; + bool testRegions = true; + bool twitchAuto = false; + bool amazonIVSAuto = false; + bool regionUS = true; + bool regionEU = true; + bool regionAsia = true; + bool regionOther = true; + bool preferHighFPS = false; + bool preferHardware = false; + int specificFPSNum = 0; + int specificFPSDen = 0; + + void TestHardwareEncoding(); + bool CanTestServer(const char *server); + + virtual void done(int result) override; + + void SaveStreamSettings(); + void SaveSettings(); + +public: + AutoConfig(QWidget *parent); + ~AutoConfig(); + + enum Page { + StartPage, + VideoPage, + StreamPage, + TestPage, + }; +}; + +class AutoConfigStartPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigStartPage(QWidget *parent = nullptr); + ~AutoConfigStartPage(); + + virtual int nextId() const override; + +public slots: + void on_prioritizeStreaming_clicked(); + void on_prioritizeRecording_clicked(); + void PrioritizeVCam(); +}; + +class AutoConfigVideoPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigVideoPage(QWidget *parent = nullptr); + ~AutoConfigVideoPage(); + + virtual int nextId() const override; + virtual bool validatePage() override; +}; + +class AutoConfigStreamPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + enum class Section : int { + Connect, + StreamKey, + }; + + std::shared_ptr auth; + + std::unique_ptr ui; + QString lastService; + bool ready = false; + + void LoadServices(bool showAll); + inline bool IsCustomService() const; + +public: + AutoConfigStreamPage(QWidget *parent = nullptr); + ~AutoConfigStreamPage(); + + virtual bool isComplete() const override; + virtual int nextId() const override; + virtual bool validatePage() override; + + void OnAuthConnected(); + void OnOAuthStreamKeyConnected(); + +public slots: + void on_show_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_preferHardware_clicked(); + void ServiceChanged(); + void UpdateKeyLink(); + void UpdateMoreInfoLink(); + void UpdateServerList(); + void UpdateCompleted(); + + void reset_service_ui_fields(std::string &service); +}; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished, + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/frontend/wizards/AutoConfigStreamPage.cpp b/frontend/wizards/AutoConfigStreamPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigStreamPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#include "moc_window-basic-auto-config.cpp" +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "url-push-button.hpp" + +#include "goliveapi-postdata.hpp" +#include "goliveapi-network.hpp" +#include "multitrack-video-error.hpp" + +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigVideoPage.h" +#include "ui_AutoConfigStreamPage.h" + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "auth-oauth.hpp" +#include "ui-config.h" +#ifdef YOUTUBE_ENABLED +#include "youtube-api-wrappers.hpp" +#endif + +struct QCef; +struct QCefCookieManager; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +#define wiz reinterpret_cast(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + if (!std::filesystem::exists(jsonFilePath)) { + return OBSData(); + } + + OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + return settings.Get(); +} + +static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) +{ + OBSData settings = OpenServiceSettings(type); + + service = obs_data_get_string(settings, "service"); + server = obs_data_get_string(settings, "server"); + key = obs_data_get_string(settings, "key"); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.StartPage")); + setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); + + OBSBasic *main = OBSBasic::Get(); + if (main->VCamEnabled()) { + QRadioButton *prioritizeVCam = + new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); + QBoxLayout *box = reinterpret_cast(layout()); + box->insertWidget(2, prioritizeVCam); + + connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); + } +} + +AutoConfigStartPage::~AutoConfigStartPage() {} + +int AutoConfigStartPage::nextId() const +{ + return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; +} + +void AutoConfigStartPage::on_prioritizeStreaming_clicked() +{ + wiz->type = AutoConfig::Type::Streaming; +} + +void AutoConfigStartPage::on_prioritizeRecording_clicked() +{ + wiz->type = AutoConfig::Type::Recording; +} + +void AutoConfigStartPage::PrioritizeVCam() +{ + wiz->type = AutoConfig::Type::VirtualCam; +} + +/* ------------------------------------------------------------------------- */ + +#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x +#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") +#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") +#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") +#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") +#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") + +AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) +{ + ui->setupUi(this); + + setTitle(QTStr("Basic.AutoConfig.VideoPage")); + setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; + + QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); + + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); + ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); + ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); + ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); + ui->fps->setCurrentIndex(0); + + QString cxStr = QString::number(ovi.base_width); + QString cyStr = QString::number(ovi.base_height); + + int encRes = int(ovi.base_width << 16) | int(ovi.base_height); + + // Auto config only supports testing down to 240p, don't allow current + // resolution if it's lower than that. + if (ovi.base_height >= 240) + ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); + + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QSize as = screen->size(); + int as_width = as.width(); + int as_height = as.height(); + + // Calculate physical screen resolution based on the virtual screen resolution + // They might differ if scaling is enabled, e.g. for HiDPI screens + as_width = round(as_width * screen->devicePixelRatio()); + as_height = round(as_height * screen->devicePixelRatio()); + + encRes = as_width << 16 | as_height; + + QString str = + QTStr(RES_USE_DISPLAY) + .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); + + ui->canvasRes->addItem(str, encRes); + } + + auto addRes = [&](int cx, int cy) { + encRes = (cx << 16) | cy; + QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); + ui->canvasRes->addItem(str, encRes); + }; + + addRes(1920, 1080); + addRes(1280, 720); + + ui->canvasRes->setCurrentIndex(0); +} + +AutoConfigVideoPage::~AutoConfigVideoPage() {} + +int AutoConfigVideoPage::nextId() const +{ + return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; +} + +bool AutoConfigVideoPage::validatePage() +{ + int encRes = ui->canvasRes->currentData().toInt(); + wiz->baseResolutionCX = encRes >> 16; + wiz->baseResolutionCY = encRes & 0xFFFF; + wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + switch (wiz->fpsType) { + case AutoConfig::FPSType::PreferHighFPS: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = true; + break; + case AutoConfig::FPSType::PreferHighRes: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::UseCurrent: + wiz->specificFPSNum = ovi.fps_num; + wiz->specificFPSDen = ovi.fps_den; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps30: + wiz->specificFPSNum = 30; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps60: + wiz->specificFPSNum = 60; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + } + + return true; +} + +/* ------------------------------------------------------------------------- */ + +enum class ListOpt : int { + ShowAll = 1, + Custom, +}; + +AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) +{ + ui->setupUi(this); + ui->bitrateLabel->setVisible(false); + ui->bitrate->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(false); + ui->useMultitrackVideo->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + int vertSpacing = ui->topLayout->verticalSpacing(); + + QMargins m = ui->topLayout->contentsMargins(); + m.setBottom(vertSpacing / 2); + ui->topLayout->setContentsMargins(m); + + m = ui->loginPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->loginPageLayout->setContentsMargins(m); + + m = ui->streamkeyPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->streamkeyPageLayout->setContentsMargins(m); + + setTitle(QTStr("Basic.AutoConfig.StreamPage")); + setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); + + LoadServices(false); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); + + connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + }); + + connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); +} + +AutoConfigStreamPage::~AutoConfigStreamPage() {} + +bool AutoConfigStreamPage::isComplete() const +{ + return ready; +} + +int AutoConfigStreamPage::nextId() const +{ + return AutoConfig::TestPage; +} + +inline bool AutoConfigStreamPage::IsCustomService() const +{ + return ui->service->currentData().toInt() == (int)ListOpt::Custom; +} + +bool AutoConfigStreamPage::validatePage() +{ + OBSDataAutoRelease service_settings = obs_data_create(); + + wiz->customServer = IsCustomService(); + + const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; + + if (!wiz->customServer) { + obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); + } + + OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); + + int bitrate; + if (!ui->doBandwidthTest->isChecked()) { + bitrate = ui->bitrate->value(); + wiz->idealBitrate = bitrate; + } else { + /* Default test target is 10 Mbps */ + bitrate = 10000; +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(wiz->serviceName)) { + /* Adjust upper bound to YouTube limits + * for resolutions above 1080p */ + if (wiz->baseResolutionCY > 1440) + bitrate = 51000; + else if (wiz->baseResolutionCY > 1080) + bitrate = 18000; + } +#endif + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", bitrate); + obs_service_apply_encoder_settings(service, settings, nullptr); + + if (wiz->customServer) { + QString server = ui->customServer->text().trimmed(); + wiz->server = wiz->serverName = QT_TO_UTF8(server); + } else { + wiz->serverName = QT_TO_UTF8(ui->server->currentText()); + wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); + } + + wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); + wiz->idealBitrate = wiz->startingBitrate; + wiz->regionUS = ui->regionUS->isChecked(); + wiz->regionEU = ui->regionEU->isChecked(); + wiz->regionAsia = ui->regionAsia->isChecked(); + wiz->regionOther = ui->regionOther->isChecked(); + wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); + if (ui->preferHardware) + wiz->preferHardware = ui->preferHardware->isChecked(); + wiz->key = QT_TO_UTF8(ui->key->text()); + + if (!wiz->customServer) { + if (wiz->serviceName == "Twitch") + wiz->service = AutoConfig::Service::Twitch; +#ifdef YOUTUBE_ENABLED + else if (IsYouTubeService(wiz->serviceName)) + wiz->service = AutoConfig::Service::YouTube; +#endif + else if (wiz->serviceName == "Amazon IVS") + wiz->service = AutoConfig::Service::AmazonIVS; + else + wiz->service = AutoConfig::Service::Other; + } else { + wiz->service = AutoConfig::Service::Other; + } + + if (wiz->service == AutoConfig::Service::Twitch) { + wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); + + if (wiz->testMultitrackVideo) { + auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, + std::nullopt, false); + + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + try { + auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), + postData, multitrack_video_name); + + for (const auto &endpoint : config.ingest_endpoints) { + if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) + continue; + + std::string address = endpoint.url_template; + auto pos = address.find("/{stream_key}"); + if (pos != address.npos) + address.erase(pos); + + wiz->serviceConfigServers.push_back({address, address}); + } + + int multitrackVideoBitrate = 0; + for (auto &encoder_config : config.encoder_configurations) { + auto it = encoder_config.settings.find("bitrate"); + if (it == encoder_config.settings.end()) + continue; + + if (!it->is_number_integer()) + continue; + + int bitrate = 0; + it->get_to(bitrate); + multitrackVideoBitrate += bitrate; + } + + if (multitrackVideoBitrate > 0) { + wiz->startingBitrate = multitrackVideoBitrate; + wiz->idealBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.testSuccessful = true; + } + } catch (const MultitrackVideoError & /*err*/) { + // FIXME: do something sensible + } + } + } + + if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && + wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { + QMessageBox::StandardButton button; +#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) + button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); +#undef WARNING_TEXT + + if (button == QMessageBox::No) + return false; + } + + return true; +} + +void AutoConfigStreamPage::on_show_clicked() +{ + if (ui->key->echoMode() == QLineEdit::Password) { + ui->key->setEchoMode(QLineEdit::Normal); + ui->show->setText(QTStr("Hide")); + } else { + ui->key->setEchoMode(QLineEdit::Password); + ui->show->setText(QTStr("Show")); + } +} + +void AutoConfigStreamPage::OnOAuthStreamKeyConnected() +{ + OAuthStreamKey *a = reinterpret_cast(auth.get()); + + if (a) { + bool validKey = !a->key().empty(); + + if (validKey) + ui->key->setText(QT_UTF8(a->key().c_str())); + + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(a->service())) { + ui->key->clear(); + + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + + ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); + + YoutubeApiWrappers *ytAuth = reinterpret_cast(a); + ChannelDescription cd; + if (ytAuth->GetChannelDescription(cd)) { + ui->connectedAccountText->setText(cd.title); + + /* Create throwaway stream key for bandwidth test */ + if (ui->doBandwidthTest->isChecked()) { + StreamDescription stream = {"", "", "OBS Studio Test Stream"}; + if (ytAuth->InsertStream(stream)) { + ui->key->setText(stream.name); + } + } + } + } +#endif + } + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::OnAuthConnected() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + Auth::Type type = Auth::AuthType(service); + + if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { + OnOAuthStreamKeyConnected(); + } +} + +void AutoConfigStreamPage::on_connectAccount_clicked() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + + OAuth::DeleteCookies(service); + + auth = OAuthStreamKey::Login(this, service); + if (!!auth) { + OnAuthConnected(); + + ui->useStreamKeyAdv->setVisible(false); + } +} + +#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" +#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" + +void AutoConfigStreamPage::on_disconnectAccount_clicked() +{ + QMessageBox::StandardButton button; + + button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); + + if (button == QMessageBox::No) { + return; + } + + OBSBasic *main = OBSBasic::Get(); + + main->auth.reset(); + auth.reset(); + + std::string service = QT_TO_UTF8(ui->service->currentText()); + +#ifdef BROWSER_AVAILABLE + OAuth::DeleteCookies(service); +#endif + + reset_service_ui_fields(service); + + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->key->setText(""); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + /* Restore key link when disconnecting account */ + UpdateKeyLink(); +} + +void AutoConfigStreamPage::on_useStreamKey_clicked() +{ + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::on_preferHardware_clicked() +{ + auto *main = OBSBasic::Get(); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + + ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); + ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); + ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); +} + +static inline bool is_auth_service(const std::string &service) +{ + return Auth::AuthType(service) != Auth::Type::None; +} + +static inline bool is_external_oauth(const std::string &service) +{ + return Auth::External(service); +} + +void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) +{ +#ifdef YOUTUBE_ENABLED + // when account is already connected: + OAuthStreamKey *a = reinterpret_cast(auth.get()); + if (a && service == a->service() && IsYouTubeService(a->service())) { + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + return; + } +#endif + + bool external_oauth = is_external_oauth(service); + if (external_oauth) { + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(true); + ui->useStreamKeyAdv->setVisible(true); + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + + } else if (cef) { + QString key = ui->key->text(); + bool can_auth = is_auth_service(service); + int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; + + ui->stackedWidget->setCurrentIndex(page); + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->connectAccount2->setVisible(can_auth); + ui->useStreamKeyAdv->setVisible(false); + } else { + ui->connectAccount2->setVisible(false); + ui->useStreamKeyAdv->setVisible(false); + } + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + ui->disconnectAccount->setVisible(false); +} + +void AutoConfigStreamPage::ServiceChanged() +{ + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + if (showMore) + return; + + std::string service = QT_TO_UTF8(ui->service->currentText()); + bool regionBased = service == "Twitch"; + bool testBandwidth = ui->doBandwidthTest->isChecked(); + bool custom = IsCustomService(); + + bool ertmp_multitrack_video_available = service == "Twitch"; + + bool custom_disclaimer = false; + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (!custom) { + OBSDataAutoRelease service_settings = obs_data_create(); + obs_data_set_string(service_settings, "service", service.c_str()); + OBSServiceAutoRelease obs_service = + obs_service_create("rtmp_common", "temp service", service_settings, nullptr); + + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setText( + obs_data_get_string(service_settings, "multitrack_video_disclaimer")); + custom_disclaimer = true; + } + } + + if (!custom_disclaimer) { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); + } + + ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setText( + QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); + ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); + ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); + + reset_service_ui_fields(service); + + /* Test three closest servers if "Auto" is available for Twitch */ + if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) + regionBased = false; + + ui->streamkeyPageLayout->removeWidget(ui->serverLabel); + ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); + + if (custom) { + ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(false); + ui->serverStackedWidget->setCurrentIndex(1); + ui->serverStackedWidget->setVisible(true); + ui->serverLabel->setVisible(true); + } else { + if (!testBandwidth) + ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(regionBased && testBandwidth); + ui->serverStackedWidget->setCurrentIndex(0); + ui->serverStackedWidget->setHidden(testBandwidth); + ui->serverLabel->setHidden(testBandwidth); + } + + wiz->testRegions = regionBased && testBandwidth; + + ui->bitrateLabel->setHidden(testBandwidth); + ui->bitrate->setHidden(testBandwidth); + + OBSBasic *main = OBSBasic::Get(); + + if (main->auth) { + auto system_auth_service = main->auth->service(); + bool service_check = service.find(system_auth_service) != std::string::npos; +#ifdef YOUTUBE_ENABLED + service_check = service_check ? service_check + : IsYouTubeService(system_auth_service) && IsYouTubeService(service); +#endif + if (service_check) { + auth.reset(); + auth = main->auth; + OnAuthConnected(); + } + } + + UpdateCompleted(); +} + +void AutoConfigStreamPage::UpdateMoreInfoLink() +{ + if (IsCustomService()) { + ui->moreInfoButton->hide(); + return; + } + + QString serviceName = ui->service->currentText(); + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + const char *more_info_link = obs_data_get_string(settings, "more_info_link"); + + if (!more_info_link || (*more_info_link == '\0')) { + ui->moreInfoButton->hide(); + } else { + ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); + ui->moreInfoButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateKeyLink() +{ + QString serviceName = ui->service->currentText(); + QString customServer = ui->customServer->text().trimmed(); + QString streamKeyLink; + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + + if (customServer.contains("fbcdn.net") && IsCustomService()) { + streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; + } + + if (serviceName == "Dacast") { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); + ui->streamKeyLabel->setToolTip(""); + } else if (!IsCustomService()) { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); + ui->streamKeyLabel->setToolTip(""); + } else { + /* add tooltips for stream key */ + QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; + QString lStr = "%1 "; + + ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); + ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); + } + + if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { + ui->streamKeyButton->hide(); + } else { + ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); + ui->streamKeyButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::LoadServices(bool showAll) +{ + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_bool(settings, "show_all", showAll); + + obs_property_t *prop = obs_properties_get(props, "show_all"); + obs_property_modified(prop, settings); + + ui->service->blockSignals(true); + ui->service->clear(); + + QStringList names; + + obs_property_t *services = obs_properties_get(props, "service"); + size_t services_count = obs_property_list_item_count(services); + for (size_t i = 0; i < services_count; i++) { + const char *name = obs_property_list_item_string(services, i); + names.push_back(name); + } + + if (showAll) + names.sort(Qt::CaseInsensitive); + + for (QString &name : names) + ui->service->addItem(name); + + if (!showAll) { + ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), + QVariant((int)ListOpt::ShowAll)); + } + + ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); + + if (!lastService.isEmpty()) { + int idx = ui->service->findText(lastService); + if (idx != -1) + ui->service->setCurrentIndex(idx); + } + + obs_properties_destroy(props); + + ui->service->blockSignals(false); +} + +void AutoConfigStreamPage::UpdateServerList() +{ + QString serviceName = ui->service->currentText(); + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + + if (showMore) { + LoadServices(true); + ui->service->showPopup(); + return; + } else { + lastService = serviceName; + } + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + obs_property_t *servers = obs_properties_get(props, "server"); + + ui->server->clear(); + + size_t servers_count = obs_property_list_item_count(servers); + for (size_t i = 0; i < servers_count; i++) { + const char *name = obs_property_list_item_name(servers, i); + const char *server = obs_property_list_item_string(servers, i); + ui->server->addItem(name, server); + } + + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateCompleted() +{ + const bool custom = IsCustomService(); + if (ui->stackedWidget->currentIndex() == (int)Section::Connect || + (ui->key->text().isEmpty() && !auth && !custom)) { + ready = false; + } else { + if (custom) { + ready = !ui->customServer->text().isEmpty(); + } else { + ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || + ui->regionAsia->isChecked() || ui->regionOther->isChecked(); + } + } + emit completeChanged(); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) +{ + EnableThreadedMessageBoxes(true); + + calldata_t cd = {0}; + calldata_set_int(&cd, "seconds", 5); + + proc_handler_t *ph = obs_get_proc_handler(); + proc_handler_call(ph, "twitch_ingests_refresh", &cd); + proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); + calldata_free(&cd); + + OBSBasic *main = reinterpret_cast(parent); + main->EnableOutputs(false); + + installEventFilter(CreateShortcutFilter()); + + std::string serviceType; + GetServiceInfo(serviceType, serviceName, server, key); +#if defined(_WIN32) || defined(__APPLE__) + setWizardStyle(QWizard::ModernStyle); +#endif + streamPage = new AutoConfigStreamPage(); + + setPage(StartPage, new AutoConfigStartPage()); + setPage(VideoPage, new AutoConfigVideoPage()); + setPage(StreamPage, streamPage); + setPage(TestPage, new AutoConfigTestPage()); + setWindowTitle(QTStr("Basic.AutoConfig")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + baseResolutionCX = ovi.base_width; + baseResolutionCY = ovi.base_height; + + /* ----------------------------------------- */ + /* check to see if Twitch's "auto" available */ + + OBSDataAutoRelease twitchSettings = obs_data_create(); + + obs_data_set_string(twitchSettings, "service", "Twitch"); + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, twitchSettings); + + obs_property_t *p = obs_properties_get(props, "server"); + const char *first = obs_property_list_item_string(p, 0); + twitchAuto = strcmp(first, "auto") == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* check to see if Amazon IVS "auto" entries are available */ + + OBSDataAutoRelease amazonIVSSettings = obs_data_create(); + + obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); + + props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, amazonIVSSettings); + + p = obs_properties_get(props, "server"); + first = obs_property_list_item_string(p, 0); + amazonIVSAuto = strncmp(first, "auto", 4) == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* load service/servers */ + + customServer = serviceType == "rtmp_custom"; + + QComboBox *serviceList = streamPage->ui->service; + + if (!serviceName.empty()) { + serviceList->blockSignals(true); + + int count = serviceList->count(); + bool found = false; + + for (int i = 0; i < count; i++) { + QString name = serviceList->itemText(i); + + if (name == serviceName.c_str()) { + serviceList->setCurrentIndex(i); + found = true; + break; + } + } + + if (!found) { + serviceList->insertItem(0, serviceName.c_str()); + serviceList->setCurrentIndex(0); + } + + serviceList->blockSignals(false); + } + + streamPage->UpdateServerList(); + streamPage->UpdateKeyLink(); + streamPage->UpdateMoreInfoLink(); + streamPage->lastService.clear(); + + if (!customServer) { + QComboBox *serverList = streamPage->ui->server; + int idx = serverList->findData(QString(server.c_str())); + if (idx == -1) + idx = 0; + + serverList->setCurrentIndex(idx); + } else { + streamPage->ui->customServer->setText(server.c_str()); + int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); + streamPage->ui->service->setCurrentIndex(idx); + } + + if (!key.empty()) + streamPage->ui->key->setText(key.c_str()); + + TestHardwareEncoding(); + + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + streamPage->ui->bitrate->setValue(bitrate); + streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); + streamPage->ServiceChanged(); + + if (!hardwareEncodingAvailable) { + delete streamPage->ui->preferHardware; + streamPage->ui->preferHardware = nullptr; + } else { + /* Newer generations of NVENC have a high enough quality to + * bitrate ratio that if NVENC is available, it makes sense to + * just always prefer hardware encoding by default */ + bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; + streamPage->ui->preferHardware->setChecked(preferHardware); + } + + setOptions(QWizard::WizardOptions()); + setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); + setButtonText(QWizard::BackButton, QTStr("Back")); + setButtonText(QWizard::NextButton, QTStr("Next")); + setButtonText(QWizard::CancelButton, QTStr("Cancel")); +} + +AutoConfig::~AutoConfig() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + main->EnableOutputs(true); + EnableThreadedMessageBoxes(false); +} + +void AutoConfig::TestHardwareEncoding() +{ + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) { + if (strcmp(id, "ffmpeg_nvenc") == 0) + hardwareEncodingAvailable = nvencAvailable = true; + else if (strcmp(id, "obs_qsv11") == 0) + hardwareEncodingAvailable = qsvAvailable = true; + else if (strcmp(id, "h264_texture_amf") == 0) + hardwareEncodingAvailable = vceAvailable = true; +#ifdef __APPLE__ + else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + if (__builtin_available(macOS 13.0, *)) + hardwareEncodingAvailable = appleAvailable = true; +#endif + } +} + +bool AutoConfig::CanTestServer(const char *server) +{ + if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) + return true; + + if (service == Service::Twitch) { + if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || + astrcmp_n(server, "US Central:", 11) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else { + return true; + } + + return false; +} + +void AutoConfig::done(int result) +{ + QWizard::done(result); + + if (result == QDialog::Accepted) { + if (type == Type::Streaming) + SaveStreamSettings(); + SaveSettings(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) { + OBSBasic *main = OBSBasic::Get(); + main->NewYouTubeAppDock(); + } +#endif + } +} + +inline const char *AutoConfig::GetEncoderId(Encoder enc) +{ + switch (enc) { + case Encoder::NVENC: + return SIMPLE_ENCODER_NVENC; + case Encoder::QSV: + return SIMPLE_ENCODER_QSV; + case Encoder::AMD: + return SIMPLE_ENCODER_AMD; + case Encoder::Apple: + return SIMPLE_ENCODER_APPLE_H264; + default: + return SIMPLE_ENCODER_X264; + } +}; + +void AutoConfig::SaveStreamSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* ---------------------------------- */ + /* save service */ + + const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + + obs_service_t *oldService = main->GetService(); + OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); + + OBSDataAutoRelease settings = obs_data_create(); + + if (!customServer) + obs_data_set_string(settings, "service", serviceName.c_str()); + obs_data_set_string(settings, "server", server.c_str()); +#ifdef YOUTUBE_ENABLED + if (!streamPage->auth || !IsYouTubeService(serviceName)) + obs_data_set_string(settings, "key", key.c_str()); +#else + obs_data_set_string(settings, "key", key.c_str()); +#endif + + OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); + + if (!newService) + return; + + main->SetService(newService); + main->SaveService(); + main->auth = streamPage->auth; + if (!!main->auth) { + main->auth->LoadUI(); + main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); + } else { + main->SetBroadcastFlowEnabled(false); + } + + /* ---------------------------------- */ + /* save stream settings */ + + config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); + config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); + config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); + + config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); + + if (multitrackVideo.targetBitrate.has_value()) + config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", + *multitrackVideo.targetBitrate); + else + config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); + + if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && + (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + } else if (multitrackVideo.bitrate.has_value()) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); + config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", + *multitrackVideo.bitrate); + } +} + +void AutoConfig::SaveSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (recordingEncoder != Encoder::Stream) + config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); + + const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; + + config_set_string(main->Config(), "Output", "Mode", "Simple"); + config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); + config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); + config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); + config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); + config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); + + if (fpsType != FPSType::UseCurrent) { + config_set_uint(main->Config(), "Video", "FPSType", 0); + config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); + } + + main->ResetVideo(); + main->ResetOutputs(); + config_save_safe(main->Config(), "tmp", nullptr); +} diff --git a/frontend/wizards/AutoConfigStreamPage.hpp b/frontend/wizards/AutoConfigStreamPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigStreamPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class Ui_AutoConfigStartPage; +class Ui_AutoConfigVideoPage; +class Ui_AutoConfigStreamPage; +class Ui_AutoConfigTestPage; + +class AutoConfigStreamPage; +class Auth; + +class AutoConfig : public QWizard { + Q_OBJECT + + friend class AutoConfigStartPage; + friend class AutoConfigVideoPage; + friend class AutoConfigStreamPage; + friend class AutoConfigTestPage; + + enum class Type { + Invalid, + Streaming, + Recording, + VirtualCam, + }; + + enum class Service { + Twitch, + YouTube, + AmazonIVS, + Other, + }; + + enum class Encoder { + x264, + NVENC, + QSV, + AMD, + Apple, + Stream, + }; + + enum class Quality { + Stream, + High, + }; + + enum class FPSType : int { + PreferHighFPS, + PreferHighRes, + UseCurrent, + fps30, + fps60, + }; + + struct StreamServer { + std::string name; + std::string address; + }; + + static inline const char *GetEncoderId(Encoder enc); + + AutoConfigStreamPage *streamPage = nullptr; + + Service service = Service::Other; + Quality recordingQuality = Quality::Stream; + Encoder recordingEncoder = Encoder::Stream; + Encoder streamingEncoder = Encoder::x264; + Type type = Type::Streaming; + FPSType fpsType = FPSType::PreferHighFPS; + int idealBitrate = 2500; + struct { + std::optional targetBitrate; + std::optional bitrate; + bool testSuccessful = false; + } multitrackVideo; + int baseResolutionCX = 1920; + int baseResolutionCY = 1080; + int idealResolutionCX = 1280; + int idealResolutionCY = 720; + int idealFPSNum = 60; + int idealFPSDen = 1; + std::string serviceName; + std::string serverName; + std::string server; + std::vector serviceConfigServers; + std::string key; + + bool hardwareEncodingAvailable = false; + bool nvencAvailable = false; + bool qsvAvailable = false; + bool vceAvailable = false; + bool appleAvailable = false; + + int startingBitrate = 2500; + bool customServer = false; + bool bandwidthTest = false; + bool testMultitrackVideo = false; + bool testRegions = true; + bool twitchAuto = false; + bool amazonIVSAuto = false; + bool regionUS = true; + bool regionEU = true; + bool regionAsia = true; + bool regionOther = true; + bool preferHighFPS = false; + bool preferHardware = false; + int specificFPSNum = 0; + int specificFPSDen = 0; + + void TestHardwareEncoding(); + bool CanTestServer(const char *server); + + virtual void done(int result) override; + + void SaveStreamSettings(); + void SaveSettings(); + +public: + AutoConfig(QWidget *parent); + ~AutoConfig(); + + enum Page { + StartPage, + VideoPage, + StreamPage, + TestPage, + }; +}; + +class AutoConfigStartPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigStartPage(QWidget *parent = nullptr); + ~AutoConfigStartPage(); + + virtual int nextId() const override; + +public slots: + void on_prioritizeStreaming_clicked(); + void on_prioritizeRecording_clicked(); + void PrioritizeVCam(); +}; + +class AutoConfigVideoPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigVideoPage(QWidget *parent = nullptr); + ~AutoConfigVideoPage(); + + virtual int nextId() const override; + virtual bool validatePage() override; +}; + +class AutoConfigStreamPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + enum class Section : int { + Connect, + StreamKey, + }; + + std::shared_ptr auth; + + std::unique_ptr ui; + QString lastService; + bool ready = false; + + void LoadServices(bool showAll); + inline bool IsCustomService() const; + +public: + AutoConfigStreamPage(QWidget *parent = nullptr); + ~AutoConfigStreamPage(); + + virtual bool isComplete() const override; + virtual int nextId() const override; + virtual bool validatePage() override; + + void OnAuthConnected(); + void OnOAuthStreamKeyConnected(); + +public slots: + void on_show_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_preferHardware_clicked(); + void ServiceChanged(); + void UpdateKeyLink(); + void UpdateMoreInfoLink(); + void UpdateServerList(); + void UpdateCompleted(); + + void reset_service_ui_fields(std::string &service); +}; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished, + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/UI/window-basic-auto-config-test.cpp b/frontend/wizards/AutoConfigTestPage.cpp similarity index 100% rename from UI/window-basic-auto-config-test.cpp rename to frontend/wizards/AutoConfigTestPage.cpp diff --git a/frontend/wizards/AutoConfigTestPage.hpp b/frontend/wizards/AutoConfigTestPage.hpp new file mode 100644 index 000000000..5d2d414a6 --- /dev/null +++ b/frontend/wizards/AutoConfigTestPage.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include +#include +#include + +class QFormLayout; +class Ui_AutoConfigTestPage; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished, + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/frontend/wizards/AutoConfigVideoPage.cpp b/frontend/wizards/AutoConfigVideoPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#include "moc_window-basic-auto-config.cpp" +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "url-push-button.hpp" + +#include "goliveapi-postdata.hpp" +#include "goliveapi-network.hpp" +#include "multitrack-video-error.hpp" + +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigVideoPage.h" +#include "ui_AutoConfigStreamPage.h" + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "auth-oauth.hpp" +#include "ui-config.h" +#ifdef YOUTUBE_ENABLED +#include "youtube-api-wrappers.hpp" +#endif + +struct QCef; +struct QCefCookieManager; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +#define wiz reinterpret_cast(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + if (!std::filesystem::exists(jsonFilePath)) { + return OBSData(); + } + + OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + return settings.Get(); +} + +static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) +{ + OBSData settings = OpenServiceSettings(type); + + service = obs_data_get_string(settings, "service"); + server = obs_data_get_string(settings, "server"); + key = obs_data_get_string(settings, "key"); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.StartPage")); + setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); + + OBSBasic *main = OBSBasic::Get(); + if (main->VCamEnabled()) { + QRadioButton *prioritizeVCam = + new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); + QBoxLayout *box = reinterpret_cast(layout()); + box->insertWidget(2, prioritizeVCam); + + connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); + } +} + +AutoConfigStartPage::~AutoConfigStartPage() {} + +int AutoConfigStartPage::nextId() const +{ + return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; +} + +void AutoConfigStartPage::on_prioritizeStreaming_clicked() +{ + wiz->type = AutoConfig::Type::Streaming; +} + +void AutoConfigStartPage::on_prioritizeRecording_clicked() +{ + wiz->type = AutoConfig::Type::Recording; +} + +void AutoConfigStartPage::PrioritizeVCam() +{ + wiz->type = AutoConfig::Type::VirtualCam; +} + +/* ------------------------------------------------------------------------- */ + +#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x +#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") +#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") +#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") +#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") +#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") + +AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) +{ + ui->setupUi(this); + + setTitle(QTStr("Basic.AutoConfig.VideoPage")); + setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; + + QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); + + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); + ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); + ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); + ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); + ui->fps->setCurrentIndex(0); + + QString cxStr = QString::number(ovi.base_width); + QString cyStr = QString::number(ovi.base_height); + + int encRes = int(ovi.base_width << 16) | int(ovi.base_height); + + // Auto config only supports testing down to 240p, don't allow current + // resolution if it's lower than that. + if (ovi.base_height >= 240) + ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); + + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QSize as = screen->size(); + int as_width = as.width(); + int as_height = as.height(); + + // Calculate physical screen resolution based on the virtual screen resolution + // They might differ if scaling is enabled, e.g. for HiDPI screens + as_width = round(as_width * screen->devicePixelRatio()); + as_height = round(as_height * screen->devicePixelRatio()); + + encRes = as_width << 16 | as_height; + + QString str = + QTStr(RES_USE_DISPLAY) + .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); + + ui->canvasRes->addItem(str, encRes); + } + + auto addRes = [&](int cx, int cy) { + encRes = (cx << 16) | cy; + QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); + ui->canvasRes->addItem(str, encRes); + }; + + addRes(1920, 1080); + addRes(1280, 720); + + ui->canvasRes->setCurrentIndex(0); +} + +AutoConfigVideoPage::~AutoConfigVideoPage() {} + +int AutoConfigVideoPage::nextId() const +{ + return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; +} + +bool AutoConfigVideoPage::validatePage() +{ + int encRes = ui->canvasRes->currentData().toInt(); + wiz->baseResolutionCX = encRes >> 16; + wiz->baseResolutionCY = encRes & 0xFFFF; + wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + switch (wiz->fpsType) { + case AutoConfig::FPSType::PreferHighFPS: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = true; + break; + case AutoConfig::FPSType::PreferHighRes: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::UseCurrent: + wiz->specificFPSNum = ovi.fps_num; + wiz->specificFPSDen = ovi.fps_den; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps30: + wiz->specificFPSNum = 30; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps60: + wiz->specificFPSNum = 60; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + } + + return true; +} + +/* ------------------------------------------------------------------------- */ + +enum class ListOpt : int { + ShowAll = 1, + Custom, +}; + +AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) +{ + ui->setupUi(this); + ui->bitrateLabel->setVisible(false); + ui->bitrate->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(false); + ui->useMultitrackVideo->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + int vertSpacing = ui->topLayout->verticalSpacing(); + + QMargins m = ui->topLayout->contentsMargins(); + m.setBottom(vertSpacing / 2); + ui->topLayout->setContentsMargins(m); + + m = ui->loginPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->loginPageLayout->setContentsMargins(m); + + m = ui->streamkeyPageLayout->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->streamkeyPageLayout->setContentsMargins(m); + + setTitle(QTStr("Basic.AutoConfig.StreamPage")); + setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); + + LoadServices(false); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); + connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); + + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); + connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); + + connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + }); + + connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); + connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); +} + +AutoConfigStreamPage::~AutoConfigStreamPage() {} + +bool AutoConfigStreamPage::isComplete() const +{ + return ready; +} + +int AutoConfigStreamPage::nextId() const +{ + return AutoConfig::TestPage; +} + +inline bool AutoConfigStreamPage::IsCustomService() const +{ + return ui->service->currentData().toInt() == (int)ListOpt::Custom; +} + +bool AutoConfigStreamPage::validatePage() +{ + OBSDataAutoRelease service_settings = obs_data_create(); + + wiz->customServer = IsCustomService(); + + const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; + + if (!wiz->customServer) { + obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); + } + + OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); + + int bitrate; + if (!ui->doBandwidthTest->isChecked()) { + bitrate = ui->bitrate->value(); + wiz->idealBitrate = bitrate; + } else { + /* Default test target is 10 Mbps */ + bitrate = 10000; +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(wiz->serviceName)) { + /* Adjust upper bound to YouTube limits + * for resolutions above 1080p */ + if (wiz->baseResolutionCY > 1440) + bitrate = 51000; + else if (wiz->baseResolutionCY > 1080) + bitrate = 18000; + } +#endif + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", bitrate); + obs_service_apply_encoder_settings(service, settings, nullptr); + + if (wiz->customServer) { + QString server = ui->customServer->text().trimmed(); + wiz->server = wiz->serverName = QT_TO_UTF8(server); + } else { + wiz->serverName = QT_TO_UTF8(ui->server->currentText()); + wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); + } + + wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); + wiz->idealBitrate = wiz->startingBitrate; + wiz->regionUS = ui->regionUS->isChecked(); + wiz->regionEU = ui->regionEU->isChecked(); + wiz->regionAsia = ui->regionAsia->isChecked(); + wiz->regionOther = ui->regionOther->isChecked(); + wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); + if (ui->preferHardware) + wiz->preferHardware = ui->preferHardware->isChecked(); + wiz->key = QT_TO_UTF8(ui->key->text()); + + if (!wiz->customServer) { + if (wiz->serviceName == "Twitch") + wiz->service = AutoConfig::Service::Twitch; +#ifdef YOUTUBE_ENABLED + else if (IsYouTubeService(wiz->serviceName)) + wiz->service = AutoConfig::Service::YouTube; +#endif + else if (wiz->serviceName == "Amazon IVS") + wiz->service = AutoConfig::Service::AmazonIVS; + else + wiz->service = AutoConfig::Service::Other; + } else { + wiz->service = AutoConfig::Service::Other; + } + + if (wiz->service == AutoConfig::Service::Twitch) { + wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); + + if (wiz->testMultitrackVideo) { + auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, + std::nullopt, false); + + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + try { + auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), + postData, multitrack_video_name); + + for (const auto &endpoint : config.ingest_endpoints) { + if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) + continue; + + std::string address = endpoint.url_template; + auto pos = address.find("/{stream_key}"); + if (pos != address.npos) + address.erase(pos); + + wiz->serviceConfigServers.push_back({address, address}); + } + + int multitrackVideoBitrate = 0; + for (auto &encoder_config : config.encoder_configurations) { + auto it = encoder_config.settings.find("bitrate"); + if (it == encoder_config.settings.end()) + continue; + + if (!it->is_number_integer()) + continue; + + int bitrate = 0; + it->get_to(bitrate); + multitrackVideoBitrate += bitrate; + } + + if (multitrackVideoBitrate > 0) { + wiz->startingBitrate = multitrackVideoBitrate; + wiz->idealBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.testSuccessful = true; + } + } catch (const MultitrackVideoError & /*err*/) { + // FIXME: do something sensible + } + } + } + + if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && + wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { + QMessageBox::StandardButton button; +#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) + button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); +#undef WARNING_TEXT + + if (button == QMessageBox::No) + return false; + } + + return true; +} + +void AutoConfigStreamPage::on_show_clicked() +{ + if (ui->key->echoMode() == QLineEdit::Password) { + ui->key->setEchoMode(QLineEdit::Normal); + ui->show->setText(QTStr("Hide")); + } else { + ui->key->setEchoMode(QLineEdit::Password); + ui->show->setText(QTStr("Show")); + } +} + +void AutoConfigStreamPage::OnOAuthStreamKeyConnected() +{ + OAuthStreamKey *a = reinterpret_cast(auth.get()); + + if (a) { + bool validKey = !a->key().empty(); + + if (validKey) + ui->key->setText(QT_UTF8(a->key().c_str())); + + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + ui->useStreamKeyAdv->setVisible(false); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(a->service())) { + ui->key->clear(); + + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + + ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); + + YoutubeApiWrappers *ytAuth = reinterpret_cast(a); + ChannelDescription cd; + if (ytAuth->GetChannelDescription(cd)) { + ui->connectedAccountText->setText(cd.title); + + /* Create throwaway stream key for bandwidth test */ + if (ui->doBandwidthTest->isChecked()) { + StreamDescription stream = {"", "", "OBS Studio Test Stream"}; + if (ytAuth->InsertStream(stream)) { + ui->key->setText(stream.name); + } + } + } + } +#endif + } + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::OnAuthConnected() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + Auth::Type type = Auth::AuthType(service); + + if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { + OnOAuthStreamKeyConnected(); + } +} + +void AutoConfigStreamPage::on_connectAccount_clicked() +{ + std::string service = QT_TO_UTF8(ui->service->currentText()); + + OAuth::DeleteCookies(service); + + auth = OAuthStreamKey::Login(this, service); + if (!!auth) { + OnAuthConnected(); + + ui->useStreamKeyAdv->setVisible(false); + } +} + +#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" +#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" + +void AutoConfigStreamPage::on_disconnectAccount_clicked() +{ + QMessageBox::StandardButton button; + + button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); + + if (button == QMessageBox::No) { + return; + } + + OBSBasic *main = OBSBasic::Get(); + + main->auth.reset(); + auth.reset(); + + std::string service = QT_TO_UTF8(ui->service->currentText()); + +#ifdef BROWSER_AVAILABLE + OAuth::DeleteCookies(service); +#endif + + reset_service_ui_fields(service); + + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->key->setText(""); + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + + /* Restore key link when disconnecting account */ + UpdateKeyLink(); +} + +void AutoConfigStreamPage::on_useStreamKey_clicked() +{ + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + UpdateCompleted(); +} + +void AutoConfigStreamPage::on_preferHardware_clicked() +{ + auto *main = OBSBasic::Get(); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + + ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); + ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); + ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); +} + +static inline bool is_auth_service(const std::string &service) +{ + return Auth::AuthType(service) != Auth::Type::None; +} + +static inline bool is_external_oauth(const std::string &service) +{ + return Auth::External(service); +} + +void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) +{ +#ifdef YOUTUBE_ENABLED + // when account is already connected: + OAuthStreamKey *a = reinterpret_cast(auth.get()); + if (a && service == a->service() && IsYouTubeService(a->service())) { + ui->connectedAccountLabel->setVisible(true); + ui->connectedAccountText->setVisible(true); + ui->connectAccount2->setVisible(false); + ui->disconnectAccount->setVisible(true); + return; + } +#endif + + bool external_oauth = is_external_oauth(service); + if (external_oauth) { + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); + ui->connectAccount2->setVisible(true); + ui->useStreamKeyAdv->setVisible(true); + + ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); + + } else if (cef) { + QString key = ui->key->text(); + bool can_auth = is_auth_service(service); + int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; + + ui->stackedWidget->setCurrentIndex(page); + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->connectAccount2->setVisible(can_auth); + ui->useStreamKeyAdv->setVisible(false); + } else { + ui->connectAccount2->setVisible(false); + ui->useStreamKeyAdv->setVisible(false); + } + + ui->connectedAccountLabel->setVisible(false); + ui->connectedAccountText->setVisible(false); + ui->disconnectAccount->setVisible(false); +} + +void AutoConfigStreamPage::ServiceChanged() +{ + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + if (showMore) + return; + + std::string service = QT_TO_UTF8(ui->service->currentText()); + bool regionBased = service == "Twitch"; + bool testBandwidth = ui->doBandwidthTest->isChecked(); + bool custom = IsCustomService(); + + bool ertmp_multitrack_video_available = service == "Twitch"; + + bool custom_disclaimer = false; + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (!custom) { + OBSDataAutoRelease service_settings = obs_data_create(); + obs_data_set_string(service_settings, "service", service.c_str()); + OBSServiceAutoRelease obs_service = + obs_service_create("rtmp_common", "temp service", service_settings, nullptr); + + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setText( + obs_data_get_string(service_settings, "multitrack_video_disclaimer")); + custom_disclaimer = true; + } + } + + if (!custom_disclaimer) { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); + } + + ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setText( + QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); + ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); + ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); + + reset_service_ui_fields(service); + + /* Test three closest servers if "Auto" is available for Twitch */ + if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) + regionBased = false; + + ui->streamkeyPageLayout->removeWidget(ui->serverLabel); + ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); + + if (custom) { + ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(false); + ui->serverStackedWidget->setCurrentIndex(1); + ui->serverStackedWidget->setVisible(true); + ui->serverLabel->setVisible(true); + } else { + if (!testBandwidth) + ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); + + ui->region->setVisible(regionBased && testBandwidth); + ui->serverStackedWidget->setCurrentIndex(0); + ui->serverStackedWidget->setHidden(testBandwidth); + ui->serverLabel->setHidden(testBandwidth); + } + + wiz->testRegions = regionBased && testBandwidth; + + ui->bitrateLabel->setHidden(testBandwidth); + ui->bitrate->setHidden(testBandwidth); + + OBSBasic *main = OBSBasic::Get(); + + if (main->auth) { + auto system_auth_service = main->auth->service(); + bool service_check = service.find(system_auth_service) != std::string::npos; +#ifdef YOUTUBE_ENABLED + service_check = service_check ? service_check + : IsYouTubeService(system_auth_service) && IsYouTubeService(service); +#endif + if (service_check) { + auth.reset(); + auth = main->auth; + OnAuthConnected(); + } + } + + UpdateCompleted(); +} + +void AutoConfigStreamPage::UpdateMoreInfoLink() +{ + if (IsCustomService()) { + ui->moreInfoButton->hide(); + return; + } + + QString serviceName = ui->service->currentText(); + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + const char *more_info_link = obs_data_get_string(settings, "more_info_link"); + + if (!more_info_link || (*more_info_link == '\0')) { + ui->moreInfoButton->hide(); + } else { + ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); + ui->moreInfoButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateKeyLink() +{ + QString serviceName = ui->service->currentText(); + QString customServer = ui->customServer->text().trimmed(); + QString streamKeyLink; + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + + if (customServer.contains("fbcdn.net") && IsCustomService()) { + streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; + } + + if (serviceName == "Dacast") { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); + ui->streamKeyLabel->setToolTip(""); + } else if (!IsCustomService()) { + ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); + ui->streamKeyLabel->setToolTip(""); + } else { + /* add tooltips for stream key */ + QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; + QString lStr = "%1 "; + + ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); + ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); + } + + if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { + ui->streamKeyButton->hide(); + } else { + ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); + ui->streamKeyButton->show(); + } + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::LoadServices(bool showAll) +{ + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_bool(settings, "show_all", showAll); + + obs_property_t *prop = obs_properties_get(props, "show_all"); + obs_property_modified(prop, settings); + + ui->service->blockSignals(true); + ui->service->clear(); + + QStringList names; + + obs_property_t *services = obs_properties_get(props, "service"); + size_t services_count = obs_property_list_item_count(services); + for (size_t i = 0; i < services_count; i++) { + const char *name = obs_property_list_item_string(services, i); + names.push_back(name); + } + + if (showAll) + names.sort(Qt::CaseInsensitive); + + for (QString &name : names) + ui->service->addItem(name); + + if (!showAll) { + ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), + QVariant((int)ListOpt::ShowAll)); + } + + ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); + + if (!lastService.isEmpty()) { + int idx = ui->service->findText(lastService); + if (idx != -1) + ui->service->setCurrentIndex(idx); + } + + obs_properties_destroy(props); + + ui->service->blockSignals(false); +} + +void AutoConfigStreamPage::UpdateServerList() +{ + QString serviceName = ui->service->currentText(); + bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; + + if (showMore) { + LoadServices(true); + ui->service->showPopup(); + return; + } else { + lastService = serviceName; + } + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + obs_property_t *servers = obs_properties_get(props, "server"); + + ui->server->clear(); + + size_t servers_count = obs_property_list_item_count(servers); + for (size_t i = 0; i < servers_count; i++) { + const char *name = obs_property_list_item_name(servers, i); + const char *server = obs_property_list_item_string(servers, i); + ui->server->addItem(name, server); + } + + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateCompleted() +{ + const bool custom = IsCustomService(); + if (ui->stackedWidget->currentIndex() == (int)Section::Connect || + (ui->key->text().isEmpty() && !auth && !custom)) { + ready = false; + } else { + if (custom) { + ready = !ui->customServer->text().isEmpty(); + } else { + ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || + ui->regionAsia->isChecked() || ui->regionOther->isChecked(); + } + } + emit completeChanged(); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) +{ + EnableThreadedMessageBoxes(true); + + calldata_t cd = {0}; + calldata_set_int(&cd, "seconds", 5); + + proc_handler_t *ph = obs_get_proc_handler(); + proc_handler_call(ph, "twitch_ingests_refresh", &cd); + proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); + calldata_free(&cd); + + OBSBasic *main = reinterpret_cast(parent); + main->EnableOutputs(false); + + installEventFilter(CreateShortcutFilter()); + + std::string serviceType; + GetServiceInfo(serviceType, serviceName, server, key); +#if defined(_WIN32) || defined(__APPLE__) + setWizardStyle(QWizard::ModernStyle); +#endif + streamPage = new AutoConfigStreamPage(); + + setPage(StartPage, new AutoConfigStartPage()); + setPage(VideoPage, new AutoConfigVideoPage()); + setPage(StreamPage, streamPage); + setPage(TestPage, new AutoConfigTestPage()); + setWindowTitle(QTStr("Basic.AutoConfig")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + baseResolutionCX = ovi.base_width; + baseResolutionCY = ovi.base_height; + + /* ----------------------------------------- */ + /* check to see if Twitch's "auto" available */ + + OBSDataAutoRelease twitchSettings = obs_data_create(); + + obs_data_set_string(twitchSettings, "service", "Twitch"); + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, twitchSettings); + + obs_property_t *p = obs_properties_get(props, "server"); + const char *first = obs_property_list_item_string(p, 0); + twitchAuto = strcmp(first, "auto") == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* check to see if Amazon IVS "auto" entries are available */ + + OBSDataAutoRelease amazonIVSSettings = obs_data_create(); + + obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); + + props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, amazonIVSSettings); + + p = obs_properties_get(props, "server"); + first = obs_property_list_item_string(p, 0); + amazonIVSAuto = strncmp(first, "auto", 4) == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* load service/servers */ + + customServer = serviceType == "rtmp_custom"; + + QComboBox *serviceList = streamPage->ui->service; + + if (!serviceName.empty()) { + serviceList->blockSignals(true); + + int count = serviceList->count(); + bool found = false; + + for (int i = 0; i < count; i++) { + QString name = serviceList->itemText(i); + + if (name == serviceName.c_str()) { + serviceList->setCurrentIndex(i); + found = true; + break; + } + } + + if (!found) { + serviceList->insertItem(0, serviceName.c_str()); + serviceList->setCurrentIndex(0); + } + + serviceList->blockSignals(false); + } + + streamPage->UpdateServerList(); + streamPage->UpdateKeyLink(); + streamPage->UpdateMoreInfoLink(); + streamPage->lastService.clear(); + + if (!customServer) { + QComboBox *serverList = streamPage->ui->server; + int idx = serverList->findData(QString(server.c_str())); + if (idx == -1) + idx = 0; + + serverList->setCurrentIndex(idx); + } else { + streamPage->ui->customServer->setText(server.c_str()); + int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); + streamPage->ui->service->setCurrentIndex(idx); + } + + if (!key.empty()) + streamPage->ui->key->setText(key.c_str()); + + TestHardwareEncoding(); + + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + streamPage->ui->bitrate->setValue(bitrate); + streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); + streamPage->ServiceChanged(); + + if (!hardwareEncodingAvailable) { + delete streamPage->ui->preferHardware; + streamPage->ui->preferHardware = nullptr; + } else { + /* Newer generations of NVENC have a high enough quality to + * bitrate ratio that if NVENC is available, it makes sense to + * just always prefer hardware encoding by default */ + bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; + streamPage->ui->preferHardware->setChecked(preferHardware); + } + + setOptions(QWizard::WizardOptions()); + setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); + setButtonText(QWizard::BackButton, QTStr("Back")); + setButtonText(QWizard::NextButton, QTStr("Next")); + setButtonText(QWizard::CancelButton, QTStr("Cancel")); +} + +AutoConfig::~AutoConfig() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + main->EnableOutputs(true); + EnableThreadedMessageBoxes(false); +} + +void AutoConfig::TestHardwareEncoding() +{ + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) { + if (strcmp(id, "ffmpeg_nvenc") == 0) + hardwareEncodingAvailable = nvencAvailable = true; + else if (strcmp(id, "obs_qsv11") == 0) + hardwareEncodingAvailable = qsvAvailable = true; + else if (strcmp(id, "h264_texture_amf") == 0) + hardwareEncodingAvailable = vceAvailable = true; +#ifdef __APPLE__ + else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + if (__builtin_available(macOS 13.0, *)) + hardwareEncodingAvailable = appleAvailable = true; +#endif + } +} + +bool AutoConfig::CanTestServer(const char *server) +{ + if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) + return true; + + if (service == Service::Twitch) { + if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || + astrcmp_n(server, "US Central:", 11) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else { + return true; + } + + return false; +} + +void AutoConfig::done(int result) +{ + QWizard::done(result); + + if (result == QDialog::Accepted) { + if (type == Type::Streaming) + SaveStreamSettings(); + SaveSettings(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) { + OBSBasic *main = OBSBasic::Get(); + main->NewYouTubeAppDock(); + } +#endif + } +} + +inline const char *AutoConfig::GetEncoderId(Encoder enc) +{ + switch (enc) { + case Encoder::NVENC: + return SIMPLE_ENCODER_NVENC; + case Encoder::QSV: + return SIMPLE_ENCODER_QSV; + case Encoder::AMD: + return SIMPLE_ENCODER_AMD; + case Encoder::Apple: + return SIMPLE_ENCODER_APPLE_H264; + default: + return SIMPLE_ENCODER_X264; + } +}; + +void AutoConfig::SaveStreamSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* ---------------------------------- */ + /* save service */ + + const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + + obs_service_t *oldService = main->GetService(); + OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); + + OBSDataAutoRelease settings = obs_data_create(); + + if (!customServer) + obs_data_set_string(settings, "service", serviceName.c_str()); + obs_data_set_string(settings, "server", server.c_str()); +#ifdef YOUTUBE_ENABLED + if (!streamPage->auth || !IsYouTubeService(serviceName)) + obs_data_set_string(settings, "key", key.c_str()); +#else + obs_data_set_string(settings, "key", key.c_str()); +#endif + + OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); + + if (!newService) + return; + + main->SetService(newService); + main->SaveService(); + main->auth = streamPage->auth; + if (!!main->auth) { + main->auth->LoadUI(); + main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); + } else { + main->SetBroadcastFlowEnabled(false); + } + + /* ---------------------------------- */ + /* save stream settings */ + + config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); + config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); + config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); + + config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); + + if (multitrackVideo.targetBitrate.has_value()) + config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", + *multitrackVideo.targetBitrate); + else + config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); + + if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && + (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + } else if (multitrackVideo.bitrate.has_value()) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); + config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", + *multitrackVideo.bitrate); + } +} + +void AutoConfig::SaveSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (recordingEncoder != Encoder::Stream) + config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); + + const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; + + config_set_string(main->Config(), "Output", "Mode", "Simple"); + config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); + config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); + config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); + config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); + config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); + + if (fpsType != FPSType::UseCurrent) { + config_set_uint(main->Config(), "Video", "FPSType", 0); + config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); + } + + main->ResetVideo(); + main->ResetOutputs(); + config_save_safe(main->Config(), "tmp", nullptr); +} diff --git a/frontend/wizards/AutoConfigVideoPage.hpp b/frontend/wizards/AutoConfigVideoPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class Ui_AutoConfigStartPage; +class Ui_AutoConfigVideoPage; +class Ui_AutoConfigStreamPage; +class Ui_AutoConfigTestPage; + +class AutoConfigStreamPage; +class Auth; + +class AutoConfig : public QWizard { + Q_OBJECT + + friend class AutoConfigStartPage; + friend class AutoConfigVideoPage; + friend class AutoConfigStreamPage; + friend class AutoConfigTestPage; + + enum class Type { + Invalid, + Streaming, + Recording, + VirtualCam, + }; + + enum class Service { + Twitch, + YouTube, + AmazonIVS, + Other, + }; + + enum class Encoder { + x264, + NVENC, + QSV, + AMD, + Apple, + Stream, + }; + + enum class Quality { + Stream, + High, + }; + + enum class FPSType : int { + PreferHighFPS, + PreferHighRes, + UseCurrent, + fps30, + fps60, + }; + + struct StreamServer { + std::string name; + std::string address; + }; + + static inline const char *GetEncoderId(Encoder enc); + + AutoConfigStreamPage *streamPage = nullptr; + + Service service = Service::Other; + Quality recordingQuality = Quality::Stream; + Encoder recordingEncoder = Encoder::Stream; + Encoder streamingEncoder = Encoder::x264; + Type type = Type::Streaming; + FPSType fpsType = FPSType::PreferHighFPS; + int idealBitrate = 2500; + struct { + std::optional targetBitrate; + std::optional bitrate; + bool testSuccessful = false; + } multitrackVideo; + int baseResolutionCX = 1920; + int baseResolutionCY = 1080; + int idealResolutionCX = 1280; + int idealResolutionCY = 720; + int idealFPSNum = 60; + int idealFPSDen = 1; + std::string serviceName; + std::string serverName; + std::string server; + std::vector serviceConfigServers; + std::string key; + + bool hardwareEncodingAvailable = false; + bool nvencAvailable = false; + bool qsvAvailable = false; + bool vceAvailable = false; + bool appleAvailable = false; + + int startingBitrate = 2500; + bool customServer = false; + bool bandwidthTest = false; + bool testMultitrackVideo = false; + bool testRegions = true; + bool twitchAuto = false; + bool amazonIVSAuto = false; + bool regionUS = true; + bool regionEU = true; + bool regionAsia = true; + bool regionOther = true; + bool preferHighFPS = false; + bool preferHardware = false; + int specificFPSNum = 0; + int specificFPSDen = 0; + + void TestHardwareEncoding(); + bool CanTestServer(const char *server); + + virtual void done(int result) override; + + void SaveStreamSettings(); + void SaveSettings(); + +public: + AutoConfig(QWidget *parent); + ~AutoConfig(); + + enum Page { + StartPage, + VideoPage, + StreamPage, + TestPage, + }; +}; + +class AutoConfigStartPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigStartPage(QWidget *parent = nullptr); + ~AutoConfigStartPage(); + + virtual int nextId() const override; + +public slots: + void on_prioritizeStreaming_clicked(); + void on_prioritizeRecording_clicked(); + void PrioritizeVCam(); +}; + +class AutoConfigVideoPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigVideoPage(QWidget *parent = nullptr); + ~AutoConfigVideoPage(); + + virtual int nextId() const override; + virtual bool validatePage() override; +}; + +class AutoConfigStreamPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + enum class Section : int { + Connect, + StreamKey, + }; + + std::shared_ptr auth; + + std::unique_ptr ui; + QString lastService; + bool ready = false; + + void LoadServices(bool showAll); + inline bool IsCustomService() const; + +public: + AutoConfigStreamPage(QWidget *parent = nullptr); + ~AutoConfigStreamPage(); + + virtual bool isComplete() const override; + virtual int nextId() const override; + virtual bool validatePage() override; + + void OnAuthConnected(); + void OnOAuthStreamKeyConnected(); + +public slots: + void on_show_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_preferHardware_clicked(); + void ServiceChanged(); + void UpdateKeyLink(); + void UpdateMoreInfoLink(); + void UpdateServerList(); + void UpdateCompleted(); + + void reset_service_ui_fields(std::string &service); +}; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished, + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/frontend/wizards/TestMode.hpp b/frontend/wizards/TestMode.hpp new file mode 100644 index 000000000..b53f37aa2 --- /dev/null +++ b/frontend/wizards/TestMode.hpp @@ -0,0 +1,1261 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "window-basic-auto-config.hpp" +#include "window-basic-main.hpp" +#include "obs-app.hpp" + +#include "ui_AutoConfigTestPage.h" + +#define wiz reinterpret_cast(wizard()) + +using namespace std; + +/* ------------------------------------------------------------------------- */ + +class TestMode { + obs_video_info ovi; + OBSSource source[6]; + + static void render_rand(void *, uint32_t cx, uint32_t cy) + { + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *randomvals[3] = {gs_effect_get_param_by_name(solid, "randomvals1"), + gs_effect_get_param_by_name(solid, "randomvals2"), + gs_effect_get_param_by_name(solid, "randomvals3")}; + + struct vec4 r; + + for (int i = 0; i < 3; i++) { + vec4_set(&r, rand_float(true) * 100.0f, rand_float(true) * 100.0f, + rand_float(true) * 50000.0f + 10000.0f, 0.0f); + gs_effect_set_vec4(randomvals[i], &r); + } + + while (gs_effect_loop(solid, "Random")) + gs_draw_sprite(nullptr, 0, cx, cy); + } + +public: + inline TestMode() + { + obs_get_video_info(&ovi); + obs_add_main_render_callback(render_rand, this); + + for (uint32_t i = 0; i < 6; i++) { + source[i] = obs_get_output_source(i); + obs_source_release(source[i]); + obs_set_output_source(i, nullptr); + } + } + + inline ~TestMode() + { + for (uint32_t i = 0; i < 6; i++) + obs_set_output_source(i, source[i]); + + obs_remove_main_render_callback(render_rand, this); + obs_reset_video(&ovi); + } + + inline void SetVideo(int cx, int cy, int fps_num, int fps_den) + { + obs_video_info newOVI = ovi; + + newOVI.output_width = (uint32_t)cx; + newOVI.output_height = (uint32_t)cy; + newOVI.fps_num = (uint32_t)fps_num; + newOVI.fps_den = (uint32_t)fps_den; + + obs_reset_video(&newOVI); + } +}; + +/* ------------------------------------------------------------------------- */ + +#define TEST_STR(x) "Basic.AutoConfig.TestPage." x +#define SUBTITLE_TESTING TEST_STR("SubTitle.Testing") +#define SUBTITLE_COMPLETE TEST_STR("SubTitle.Complete") +#define TEST_BW TEST_STR("TestingBandwidth") +#define TEST_BW_NO_OUTPUT TEST_STR("TestingBandwidth.NoOutput") +#define TEST_BW_CONNECTING TEST_STR("TestingBandwidth.Connecting") +#define TEST_BW_CONNECT_FAIL TEST_STR("TestingBandwidth.ConnectFailed") +#define TEST_BW_SERVER TEST_STR("TestingBandwidth.Server") +#define TEST_RES_VAL TEST_STR("TestingRes.Resolution") +#define TEST_RES_FAIL TEST_STR("TestingRes.Fail") +#define TEST_SE TEST_STR("TestingStreamEncoder") +#define TEST_RE TEST_STR("TestingRecordingEncoder") +#define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder") +#define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder") + +void AutoConfigTestPage::StartBandwidthStage() +{ + ui->progressLabel->setText(QTStr(TEST_BW)); + testThread = std::thread([this]() { TestBandwidthThread(); }); +} + +void AutoConfigTestPage::StartStreamEncoderStage() +{ + ui->progressLabel->setText(QTStr(TEST_SE)); + testThread = std::thread([this]() { TestStreamEncoderThread(); }); +} + +void AutoConfigTestPage::StartRecordingEncoderStage() +{ + ui->progressLabel->setText(QTStr(TEST_RE)); + testThread = std::thread([this]() { TestRecordingEncoderThread(); }); +} + +void AutoConfigTestPage::GetServers(std::vector &servers) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "service", wiz->serviceName.c_str()); + + obs_properties_t *ppts = obs_get_service_properties("rtmp_common"); + obs_property_t *p = obs_properties_get(ppts, "service"); + obs_property_modified(p, settings); + + p = obs_properties_get(ppts, "server"); + size_t count = obs_property_list_item_count(p); + servers.reserve(count); + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + const char *server = obs_property_list_item_string(p, i); + + if (wiz->CanTestServer(name)) { + ServerInfo info(name, server); + servers.push_back(info); + } + } + + obs_properties_destroy(ppts); +} + +static inline void string_depad_key(string &key) +{ + while (!key.empty()) { + char ch = key.back(); + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') + key.pop_back(); + else + break; + } +} + +const char *FindAudioEncoderFromCodec(const char *type); + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +void AutoConfigTestPage::TestBandwidthThread() +{ + bool connected = false; + bool stopped = false; + + TestMode testMode; + testMode.SetVideo(128, 128, 60, 1); + + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, 0)); + + /* + * create encoders + * create output + * test for 10 seconds + */ + + QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral(""))); + + /* -----------------------------------*/ + /* create obs objects */ + + const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; + + OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr); + OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); + OBSServiceAutoRelease service = obs_service_create(serverType, "test_service", nullptr, nullptr); + + /* -----------------------------------*/ + /* configure settings */ + + // service: "service", "server", "key" + // vencoder: "bitrate", "rate_control", + // obs_service_apply_encoder_settings + // aencoder: "bitrate" + // output: "bind_ip" via main config -> "Output", "BindIP" + // obs_output_set_service + + OBSDataAutoRelease service_settings = obs_data_create(); + OBSDataAutoRelease vencoder_settings = obs_data_create(); + OBSDataAutoRelease aencoder_settings = obs_data_create(); + OBSDataAutoRelease output_settings = obs_data_create(); + + std::string key = wiz->key; + if (wiz->service == AutoConfig::Service::Twitch || wiz->service == AutoConfig::Service::AmazonIVS) { + string_depad_key(key); + key += "?bandwidthtest"; + } else if (wiz->serviceName == "Restream.io" || wiz->serviceName == "Restream.io - RTMP") { + string_depad_key(key); + key += "?test=true"; + } + + obs_data_set_string(service_settings, "service", wiz->serviceName.c_str()); + obs_data_set_string(service_settings, "key", key.c_str()); + + obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); + obs_data_set_string(vencoder_settings, "rate_control", "CBR"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + obs_data_set_int(vencoder_settings, "keyint_sec", 2); + + obs_data_set_int(aencoder_settings, "bitrate", 32); + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *bind_ip = config_get_string(main->Config(), "Output", "BindIP"); + obs_data_set_string(output_settings, "bind_ip", bind_ip); + + const char *ip_family = config_get_string(main->Config(), "Output", "IPFamily"); + obs_data_set_string(output_settings, "ip_family", ip_family); + + /* -----------------------------------*/ + /* determine which servers to test */ + + std::vector servers; + if (wiz->customServer) + servers.emplace_back(wiz->server.c_str(), wiz->server.c_str()); + else + GetServers(servers); + + /* just use the first server if it only has one alternate server, + * or if using Restream or Nimo TV due to their "auto" servers */ + if (servers.size() < 3 || wiz->serviceName.substr(0, 11) == "Restream.io" || wiz->serviceName == "Nimo TV") { + servers.resize(1); + + } else if ((wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) || + (wiz->service == AutoConfig::Service::AmazonIVS && wiz->amazonIVSAuto)) { + /* if using Twitch and "Auto" is available, test 3 closest + * server */ + servers.erase(servers.begin() + 1); + servers.resize(3); + } else if (wiz->service == AutoConfig::Service::YouTube) { + /* Only test first set of primary + backup servers */ + servers.resize(2); + } + + if (!wiz->serviceConfigServers.empty()) { + if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) { + // servers from Twitch service config replace the "auto" entry + servers.erase(servers.begin()); + } + + for (auto it = std::rbegin(wiz->serviceConfigServers); it != std::rend(wiz->serviceConfigServers); + it++) { + auto same_server = + std::find_if(std::begin(servers), std::end(servers), + [&](const ServerInfo &si) { return si.address == it->address; }); + if (same_server != std::end(servers)) + servers.erase(same_server); + servers.emplace(std::begin(servers), it->name.c_str(), it->address.c_str()); + } + + if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) { + // see above, only test 3 servers + // rtmps urls are currently counted as separate servers + servers.resize(3); + } + } + + /* -----------------------------------*/ + /* apply service settings */ + + obs_service_update(service, service_settings); + obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings); + + if (wiz->multitrackVideo.testSuccessful) { + obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); + } + + /* -----------------------------------*/ + /* create output */ + + /* Check if the service has a preferred output type */ + const char *output_type = obs_service_get_preferred_output_type(service); + if (!output_type || (obs_get_output_flags(output_type) & OBS_OUTPUT_SERVICE) == 0) { + /* Otherwise, prefer first-party output types */ + const char *protocol = obs_service_get_protocol(service); + + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + output_type = "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + output_type = "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + output_type = "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + if (!output_type) + obs_enum_output_types_with_protocol(protocol, &output_type, return_first_id); + + /* If none, fail */ + if (!output_type) { + QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_NO_OUTPUT))); + return; + } + } + + OBSOutputAutoRelease output = obs_output_create(output_type, "test_stream", nullptr, nullptr); + obs_output_update(output, output_settings); + + const char *audio_codec = obs_output_get_supported_audio_codecs(output); + + if (strcmp(audio_codec, "aac") != 0) { + const char *id = FindAudioEncoderFromCodec(audio_codec); + aencoder = obs_audio_encoder_create(id, "test_audio", nullptr, 0, nullptr); + } + + /* -----------------------------------*/ + /* connect encoders/services/outputs */ + + obs_encoder_update(vencoder, vencoder_settings); + obs_encoder_update(aencoder, aencoder_settings); + obs_encoder_set_video(vencoder, obs_get_video()); + obs_encoder_set_audio(aencoder, obs_get_audio()); + + obs_output_set_video_encoder(output, vencoder); + obs_output_set_audio_encoder(output, aencoder, 0); + obs_output_set_reconnect_settings(output, 0, 0); + + obs_output_set_service(output, service); + + /* -----------------------------------*/ + /* connect signals */ + + auto on_started = [&]() { + unique_lock lock(m); + connected = true; + stopped = false; + cv.notify_one(); + }; + + auto on_stopped = [&]() { + unique_lock lock(m); + connected = false; + stopped = true; + cv.notify_one(); + }; + + using on_started_t = decltype(on_started); + using on_stopped_t = decltype(on_stopped); + + auto pre_on_started = [](void *data, calldata_t *) { + on_started_t &on_started = *reinterpret_cast(data); + on_started(); + }; + + auto pre_on_stopped = [](void *data, calldata_t *) { + on_stopped_t &on_stopped = *reinterpret_cast(data); + on_stopped(); + }; + + signal_handler *sh = obs_output_get_signal_handler(output); + signal_handler_connect(sh, "start", pre_on_started, &on_started); + signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); + + /* -----------------------------------*/ + /* test servers */ + + bool success = false; + + for (size_t i = 0; i < servers.size(); i++) { + auto &server = servers[i]; + + connected = false; + stopped = false; + + int per = int((i + 1) * 100 / servers.size()); + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_BW_CONNECTING).arg(server.name.c_str()))); + + obs_data_set_string(service_settings, "server", server.address.c_str()); + obs_service_update(service, service_settings); + + if (!obs_output_start(output)) + continue; + + unique_lock ul(m); + if (cancel) { + ul.unlock(); + obs_output_force_stop(output); + return; + } + if (!stopped && !connected) + cv.wait(ul); + if (cancel) { + ul.unlock(); + obs_output_force_stop(output); + return; + } + if (!connected) + continue; + + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_BW_SERVER).arg(server.name.c_str()))); + + /* ignore first 2.5 seconds due to possible buffering skewing + * the result */ + cv.wait_for(ul, chrono::milliseconds(2500)); + if (stopped) + continue; + if (cancel) { + ul.unlock(); + obs_output_force_stop(output); + return; + } + + /* continue test */ + int start_bytes = (int)obs_output_get_total_bytes(output); + uint64_t t_start = os_gettime_ns(); + + cv.wait_for(ul, chrono::seconds(10)); + if (stopped) + continue; + if (cancel) { + ul.unlock(); + obs_output_force_stop(output); + return; + } + + obs_output_stop(output); + cv.wait(ul); + + uint64_t total_time = os_gettime_ns() - t_start; + if (total_time == 0) + total_time = 1; + + int total_bytes = (int)obs_output_get_total_bytes(output) - start_bytes; + uint64_t bitrate = util_mul_div64(total_bytes, 8ULL * 1000000000ULL / 1000ULL, total_time); + if (obs_output_get_frames_dropped(output) || (int)bitrate < (wiz->startingBitrate * 75 / 100)) { + server.bitrate = (int)bitrate * 70 / 100; + } else { + server.bitrate = wiz->startingBitrate; + } + + server.ms = obs_output_get_connect_time_ms(output); + success = true; + } + + if (!success) { + QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_CONNECT_FAIL))); + return; + } + + int bestBitrate = 0; + int bestMS = 0x7FFFFFFF; + string bestServer; + string bestServerName; + + for (auto &server : servers) { + bool close = abs(server.bitrate - bestBitrate) < 400; + + if ((!close && server.bitrate > bestBitrate) || (close && server.ms < bestMS)) { + bestServer = server.address; + bestServerName = server.name; + bestBitrate = server.bitrate; + bestMS = server.ms; + } + } + + wiz->server = std::move(bestServer); + wiz->serverName = std::move(bestServerName); + wiz->idealBitrate = bestBitrate; + + QMetaObject::invokeMethod(this, "NextStage"); +} + +/* this is used to estimate the lower bitrate limit for a given + * resolution/fps. yes, it is a totally arbitrary equation that gets + * the closest to the expected values */ +static long double EstimateBitrateVal(int cx, int cy, int fps_num, int fps_den) +{ + long fps = (long double)fps_num / (long double)fps_den; + long double areaVal = pow((long double)(cx * cy), 0.85l); + return areaVal * sqrt(pow(fps, 1.1l)); +} + +static long double EstimateMinBitrate(int cx, int cy, int fps_num, int fps_den) +{ + long double val = EstimateBitrateVal(1920, 1080, 60, 1) / 5800.0l; + return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; +} + +static long double EstimateUpperBitrate(int cx, int cy, int fps_num, int fps_den) +{ + long double val = EstimateBitrateVal(1280, 720, 30, 1) / 3000.0l; + return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; +} + +struct Result { + int cx; + int cy; + int fps_num; + int fps_den; + + inline Result(int cx_, int cy_, int fps_num_, int fps_den_) + : cx(cx_), + cy(cy_), + fps_num(fps_num_), + fps_den(fps_den_) + { + } +}; + +static void CalcBaseRes(int &baseCX, int &baseCY) +{ + const int maxBaseArea = 1920 * 1200; + const int clipResArea = 1920 * 1080; + + /* if base resolution unusually high, recalculate to a more reasonable + * value to start the downscaling at, based upon 1920x1080's area. + * + * for 16:9 resolutions this will always change the starting value to + * 1920x1080 */ + if ((baseCX * baseCY) > maxBaseArea) { + long double xyAspect = (long double)baseCX / (long double)baseCY; + baseCY = (int)sqrt((long double)clipResArea / xyAspect); + baseCX = (int)((long double)baseCY * xyAspect); + } +} + +bool AutoConfigTestPage::TestSoftwareEncoding() +{ + TestMode testMode; + QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral(""))); + + /* -----------------------------------*/ + /* create obs objects */ + + OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr); + OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); + OBSOutputAutoRelease output = obs_output_create("null_output", "null", nullptr, nullptr); + + /* -----------------------------------*/ + /* configure settings */ + + OBSDataAutoRelease aencoder_settings = obs_data_create(); + OBSDataAutoRelease vencoder_settings = obs_data_create(); + obs_data_set_int(aencoder_settings, "bitrate", 32); + + if (wiz->type != AutoConfig::Type::Recording) { + obs_data_set_int(vencoder_settings, "keyint_sec", 2); + obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); + obs_data_set_string(vencoder_settings, "rate_control", "CBR"); + obs_data_set_string(vencoder_settings, "profile", "main"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + } else { + obs_data_set_int(vencoder_settings, "crf", 20); + obs_data_set_string(vencoder_settings, "rate_control", "CRF"); + obs_data_set_string(vencoder_settings, "profile", "high"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + } + + /* -----------------------------------*/ + /* apply settings */ + + obs_encoder_update(vencoder, vencoder_settings); + obs_encoder_update(aencoder, aencoder_settings); + + /* -----------------------------------*/ + /* connect encoders/services/outputs */ + + obs_output_set_video_encoder(output, vencoder); + obs_output_set_audio_encoder(output, aencoder, 0); + + /* -----------------------------------*/ + /* connect signals */ + + auto on_stopped = [&]() { + unique_lock lock(m); + cv.notify_one(); + }; + + using on_stopped_t = decltype(on_stopped); + + auto pre_on_stopped = [](void *data, calldata_t *) { + on_stopped_t &on_stopped = *reinterpret_cast(data); + on_stopped(); + }; + + signal_handler *sh = obs_output_get_signal_handler(output); + signal_handler_connect(sh, "deactivate", pre_on_stopped, &on_stopped); + + /* -----------------------------------*/ + /* calculate starting resolution */ + + int baseCX = wiz->baseResolutionCX; + int baseCY = wiz->baseResolutionCY; + CalcBaseRes(baseCX, baseCY); + + /* -----------------------------------*/ + /* calculate starting test rates */ + + int pcores = os_get_physical_cores(); + int lcores = os_get_logical_cores(); + int maxDataRate; + if (lcores > 8 || pcores > 4) { + /* superb */ + maxDataRate = 1920 * 1200 * 60 + 1000; + + } else if (lcores > 4 && pcores == 4) { + /* great */ + maxDataRate = 1920 * 1080 * 60 + 1000; + + } else if (pcores == 4) { + /* okay */ + maxDataRate = 1920 * 1080 * 30 + 1000; + + } else { + /* toaster */ + maxDataRate = 960 * 540 * 30 + 1000; + } + + /* -----------------------------------*/ + /* perform tests */ + + vector results; + int i = 0; + int count = 1; + + auto testRes = [&](int cy, int fps_num, int fps_den, bool force) { + int per = ++i * 100 / count; + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); + + if (cy > baseCY) + return true; + + /* no need for more than 3 tests max */ + if (results.size() >= 3) + return true; + + if (!fps_num || !fps_den) { + fps_num = wiz->specificFPSNum; + fps_den = wiz->specificFPSDen; + } + + long double fps = ((long double)fps_num / (long double)fps_den); + + int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy); + + if (!force && wiz->type != AutoConfig::Type::Recording) { + int est = EstimateMinBitrate(cx, cy, fps_num, fps_den); + if (est > wiz->idealBitrate) + return true; + } + + long double rate = (long double)cx * (long double)cy * fps; + if (!force && rate > maxDataRate) + return true; + + testMode.SetVideo(cx, cy, fps_num, fps_den); + + obs_encoder_set_video(vencoder, obs_get_video()); + obs_encoder_set_audio(aencoder, obs_get_audio()); + obs_encoder_update(vencoder, vencoder_settings); + + obs_output_set_media(output, obs_get_video(), obs_get_audio()); + + QString cxStr = QString::number(cx); + QString cyStr = QString::number(cy); + + QString fpsStr = (fps_den > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2); + + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_RES_VAL).arg(cxStr, cyStr, fpsStr))); + + unique_lock ul(m); + if (cancel) + return false; + + if (!obs_output_start(output)) { + QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_RES_FAIL))); + return false; + } + + cv.wait_for(ul, chrono::seconds(5)); + + obs_output_stop(output); + cv.wait(ul); + + int skipped = (int)video_output_get_skipped_frames(obs_get_video()); + if (force || skipped <= 10) + results.emplace_back(cx, cy, fps_num, fps_den); + + return !cancel; + }; + + if (wiz->specificFPSNum && wiz->specificFPSDen) { + count = 7; + if (!testRes(2160, 0, 0, false)) + return false; + if (!testRes(1440, 0, 0, false)) + return false; + if (!testRes(1080, 0, 0, false)) + return false; + if (!testRes(720, 0, 0, false)) + return false; + if (!testRes(480, 0, 0, false)) + return false; + if (!testRes(360, 0, 0, false)) + return false; + if (!testRes(240, 0, 0, true)) + return false; + } else { + count = 14; + if (!testRes(2160, 60, 1, false)) + return false; + if (!testRes(2160, 30, 1, false)) + return false; + if (!testRes(1440, 60, 1, false)) + return false; + if (!testRes(1440, 30, 1, false)) + return false; + if (!testRes(1080, 60, 1, false)) + return false; + if (!testRes(1080, 30, 1, false)) + return false; + if (!testRes(720, 60, 1, false)) + return false; + if (!testRes(720, 30, 1, false)) + return false; + if (!testRes(480, 60, 1, false)) + return false; + if (!testRes(480, 30, 1, false)) + return false; + if (!testRes(360, 60, 1, false)) + return false; + if (!testRes(360, 30, 1, false)) + return false; + if (!testRes(240, 60, 1, false)) + return false; + if (!testRes(240, 30, 1, true)) + return false; + } + + /* -----------------------------------*/ + /* find preferred settings */ + + int minArea = 960 * 540 + 1000; + + if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { + Result &result1 = results[0]; + Result &result2 = results[1]; + + if (result1.fps_num == 30 && result2.fps_num == 60) { + int nextArea = result2.cx * result2.cy; + if (nextArea >= minArea) + results.erase(results.begin()); + } + } + + Result result = results.front(); + wiz->idealResolutionCX = result.cx; + wiz->idealResolutionCY = result.cy; + wiz->idealFPSNum = result.fps_num; + wiz->idealFPSDen = result.fps_den; + + long double fUpperBitrate = EstimateUpperBitrate(result.cx, result.cy, result.fps_num, result.fps_den); + + int upperBitrate = int(floor(fUpperBitrate / 50.0l) * 50.0l); + + if (wiz->streamingEncoder != AutoConfig::Encoder::x264) { + upperBitrate *= 114; + upperBitrate /= 100; + } + + if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful && + !wiz->multitrackVideo.bitrate.has_value()) + wiz->multitrackVideo.bitrate = wiz->idealBitrate; + + if (wiz->idealBitrate > upperBitrate) + wiz->idealBitrate = upperBitrate; + + softwareTested = true; + return true; +} + +void AutoConfigTestPage::FindIdealHardwareResolution() +{ + int baseCX = wiz->baseResolutionCX; + int baseCY = wiz->baseResolutionCY; + CalcBaseRes(baseCX, baseCY); + + vector results; + + int pcores = os_get_physical_cores(); + int maxDataRate; + if (pcores >= 4) { + maxDataRate = 1920 * 1200 * 60 + 1000; + } else { + maxDataRate = 1280 * 720 * 30 + 1000; + } + + auto testRes = [&](int cy, int fps_num, int fps_den, bool force) { + if (cy > baseCY) + return; + + if (results.size() >= 3) + return; + + if (!fps_num || !fps_den) { + fps_num = wiz->specificFPSNum; + fps_den = wiz->specificFPSDen; + } + + long double fps = ((long double)fps_num / (long double)fps_den); + + int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy); + + long double rate = (long double)cx * (long double)cy * fps; + if (!force && rate > maxDataRate) + return; + + AutoConfig::Encoder encType = wiz->streamingEncoder; + bool nvenc = encType == AutoConfig::Encoder::NVENC; + + int minBitrate = EstimateMinBitrate(cx, cy, fps_num, fps_den); + + /* most hardware encoders don't have a good quality to bitrate + * ratio, so increase the minimum bitrate estimate for them. + * NVENC currently is the exception because of the improvements + * its made to its quality in recent generations. */ + if (!nvenc) + minBitrate = minBitrate * 114 / 100; + + if (wiz->type == AutoConfig::Type::Recording) + force = true; + if (force || wiz->idealBitrate >= minBitrate) + results.emplace_back(cx, cy, fps_num, fps_den); + }; + + if (wiz->specificFPSNum && wiz->specificFPSDen) { + testRes(2160, 0, 0, false); + testRes(1440, 0, 0, false); + testRes(1080, 0, 0, false); + testRes(720, 0, 0, false); + testRes(480, 0, 0, false); + testRes(360, 0, 0, false); + testRes(240, 0, 0, true); + } else { + testRes(2160, 60, 1, false); + testRes(2160, 30, 1, false); + testRes(1440, 60, 1, false); + testRes(1440, 30, 1, false); + testRes(1080, 60, 1, false); + testRes(1080, 30, 1, false); + testRes(720, 60, 1, false); + testRes(720, 30, 1, false); + testRes(480, 60, 1, false); + testRes(480, 30, 1, false); + testRes(360, 60, 1, false); + testRes(360, 30, 1, false); + testRes(240, 60, 1, false); + testRes(240, 30, 1, true); + } + + int minArea = 960 * 540 + 1000; + + if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { + Result &result1 = results[0]; + Result &result2 = results[1]; + + if (result1.fps_num == 30 && result2.fps_num == 60) { + int nextArea = result2.cx * result2.cy; + if (nextArea >= minArea) + results.erase(results.begin()); + } + } + + Result result = results.front(); + wiz->idealResolutionCX = result.cx; + wiz->idealResolutionCY = result.cy; + wiz->idealFPSNum = result.fps_num; + wiz->idealFPSDen = result.fps_den; +} + +void AutoConfigTestPage::TestStreamEncoderThread() +{ + bool preferHardware = wiz->preferHardware; + if (!softwareTested) { + if (!preferHardware || !wiz->hardwareEncodingAvailable) { + if (!TestSoftwareEncoding()) { + return; + } + } + } + + if (!softwareTested) { + if (wiz->nvencAvailable) + wiz->streamingEncoder = AutoConfig::Encoder::NVENC; + else if (wiz->qsvAvailable) + wiz->streamingEncoder = AutoConfig::Encoder::QSV; + else if (wiz->appleAvailable) + wiz->streamingEncoder = AutoConfig::Encoder::Apple; + else + wiz->streamingEncoder = AutoConfig::Encoder::AMD; + } else { + wiz->streamingEncoder = AutoConfig::Encoder::x264; + } + +#ifdef __linux__ + // On linux CBR rate control is not guaranteed so fallback to x264. + if (wiz->streamingEncoder == AutoConfig::Encoder::QSV) { + wiz->streamingEncoder = AutoConfig::Encoder::x264; + if (!TestSoftwareEncoding()) { + return; + } + } +#endif + + if (preferHardware && !softwareTested && wiz->hardwareEncodingAvailable) + FindIdealHardwareResolution(); + + QMetaObject::invokeMethod(this, "NextStage"); +} + +void AutoConfigTestPage::TestRecordingEncoderThread() +{ + if (!wiz->hardwareEncodingAvailable && !softwareTested) { + if (!TestSoftwareEncoding()) { + return; + } + } + + if (wiz->type == AutoConfig::Type::Recording && wiz->hardwareEncodingAvailable) + FindIdealHardwareResolution(); + + wiz->recordingQuality = AutoConfig::Quality::High; + + bool recordingOnly = wiz->type == AutoConfig::Type::Recording; + + if (wiz->hardwareEncodingAvailable) { + if (wiz->nvencAvailable) + wiz->recordingEncoder = AutoConfig::Encoder::NVENC; + else if (wiz->qsvAvailable) + wiz->recordingEncoder = AutoConfig::Encoder::QSV; + else if (wiz->appleAvailable) + wiz->recordingEncoder = AutoConfig::Encoder::Apple; + else + wiz->recordingEncoder = AutoConfig::Encoder::AMD; + } else { + wiz->recordingEncoder = AutoConfig::Encoder::x264; + } + + if (wiz->recordingEncoder != AutoConfig::Encoder::NVENC) { + if (!recordingOnly) { + wiz->recordingEncoder = AutoConfig::Encoder::Stream; + wiz->recordingQuality = AutoConfig::Quality::Stream; + } + } + + QMetaObject::invokeMethod(this, "NextStage"); +} + +#define ENCODER_TEXT(x) "Basic.Settings.Output.Simple.Encoder." x +#define ENCODER_SOFTWARE ENCODER_TEXT("Software") +#define ENCODER_NVENC ENCODER_TEXT("Hardware.NVENC.H264") +#define ENCODER_QSV ENCODER_TEXT("Hardware.QSV.H264") +#define ENCODER_AMD ENCODER_TEXT("Hardware.AMD.H264") +#define ENCODER_APPLE ENCODER_TEXT("Hardware.Apple.H264") + +#define QUALITY_SAME "Basic.Settings.Output.Simple.RecordingQuality.Stream" +#define QUALITY_HIGH "Basic.Settings.Output.Simple.RecordingQuality.Small" + +void set_closest_res(int &cx, int &cy, struct obs_service_resolution *res_list, size_t count) +{ + int best_pixel_diff = 0x7FFFFFFF; + int start_cx = cx; + int start_cy = cy; + + for (size_t i = 0; i < count; i++) { + struct obs_service_resolution &res = res_list[i]; + int pixel_cx_diff = abs(start_cx - res.cx); + int pixel_cy_diff = abs(start_cy - res.cy); + int pixel_diff = pixel_cx_diff + pixel_cy_diff; + + if (pixel_diff < best_pixel_diff) { + best_pixel_diff = pixel_diff; + cx = res.cx; + cy = res.cy; + } + } +} + +void AutoConfigTestPage::FinalizeResults() +{ + ui->stackedWidget->setCurrentIndex(1); + setSubTitle(QTStr(SUBTITLE_COMPLETE)); + + QFormLayout *form = results; + + auto encName = [](AutoConfig::Encoder enc) -> QString { + switch (enc) { + case AutoConfig::Encoder::x264: + return QTStr(ENCODER_SOFTWARE); + case AutoConfig::Encoder::NVENC: + return QTStr(ENCODER_NVENC); + case AutoConfig::Encoder::QSV: + return QTStr(ENCODER_QSV); + case AutoConfig::Encoder::AMD: + return QTStr(ENCODER_AMD); + case AutoConfig::Encoder::Apple: + return QTStr(ENCODER_APPLE); + case AutoConfig::Encoder::Stream: + return QTStr(QUALITY_SAME); + } + + return QTStr(ENCODER_SOFTWARE); + }; + + auto newLabel = [this](const char *str) -> QLabel * { + return new QLabel(QTStr(str), this); + }; + + if (wiz->type == AutoConfig::Type::Streaming) { + const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; + + OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", nullptr, nullptr); + + OBSDataAutoRelease service_settings = obs_data_create(); + OBSDataAutoRelease vencoder_settings = obs_data_create(); + + if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful && + !wiz->multitrackVideo.bitrate.has_value()) + wiz->multitrackVideo.bitrate = wiz->idealBitrate; + + obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); + + obs_data_set_string(service_settings, "service", wiz->serviceName.c_str()); + obs_service_update(service, service_settings); + obs_service_apply_encoder_settings(service, vencoder_settings, nullptr); + + BPtr res_list; + size_t res_count; + int maxFPS; + obs_service_get_supported_resolutions(service, &res_list, &res_count); + obs_service_get_max_fps(service, &maxFPS); + + if (res_list) { + set_closest_res(wiz->idealResolutionCX, wiz->idealResolutionCY, res_list, res_count); + } + if (maxFPS) { + double idealFPS = (double)wiz->idealFPSNum / (double)wiz->idealFPSDen; + if (idealFPS > (double)maxFPS) { + wiz->idealFPSNum = maxFPS; + wiz->idealFPSDen = 1; + } + } + + wiz->idealBitrate = (int)obs_data_get_int(vencoder_settings, "bitrate"); + + if (!wiz->customServer) + form->addRow(newLabel("Basic.AutoConfig.StreamPage.Service"), + new QLabel(wiz->serviceName.c_str(), ui->finishPage)); + form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), + new QLabel(wiz->serverName.c_str(), ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Stream.MultitrackVideoLabel"), + newLabel(wiz->multitrackVideo.testSuccessful ? "Yes" : "No")); + + if (wiz->multitrackVideo.testSuccessful) { + form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), newLabel("Automatic")); + form->addRow(newLabel(TEST_RESULT_SE), newLabel("Automatic")); + form->addRow(newLabel("Basic.AutoConfig.TestPage.Result.StreamingResolution"), + newLabel("Automatic")); + } else { + form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), + new QLabel(QString::number(wiz->idealBitrate), ui->finishPage)); + form->addRow(newLabel(TEST_RESULT_SE), + new QLabel(encName(wiz->streamingEncoder), ui->finishPage)); + } + } + + QString baseRes = + QString("%1x%2").arg(QString::number(wiz->baseResolutionCX), QString::number(wiz->baseResolutionCY)); + QString scaleRes = + QString("%1x%2").arg(QString::number(wiz->idealResolutionCX), QString::number(wiz->idealResolutionCY)); + + if (wiz->recordingEncoder != AutoConfig::Encoder::Stream || + wiz->recordingQuality != AutoConfig::Quality::Stream) + form->addRow(newLabel(TEST_RESULT_RE), new QLabel(encName(wiz->recordingEncoder), ui->finishPage)); + + QString recQuality; + + switch (wiz->recordingQuality) { + case AutoConfig::Quality::High: + recQuality = QTStr(QUALITY_HIGH); + break; + case AutoConfig::Quality::Stream: + recQuality = QTStr(QUALITY_SAME); + break; + } + + form->addRow(newLabel("Basic.Settings.Output.Simple.RecordingQuality"), new QLabel(recQuality, ui->finishPage)); + + long double fps = (long double)wiz->idealFPSNum / (long double)wiz->idealFPSDen; + + QString fpsStr = (wiz->idealFPSDen > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2); + + form->addRow(newLabel("Basic.Settings.Video.BaseResolution"), new QLabel(baseRes, ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Video.ScaledResolution"), new QLabel(scaleRes, ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Video.FPS"), new QLabel(fpsStr, ui->finishPage)); + + // FIXME: form layout is super squished, probably need to set proper sizepolicy on all widgets? +} + +#define STARTING_SEPARATOR "\n==== Auto-config wizard testing commencing ======\n" +#define STOPPING_SEPARATOR "\n==== Auto-config wizard testing stopping ========\n" + +void AutoConfigTestPage::NextStage() +{ + if (testThread.joinable()) + testThread.join(); + if (cancel) + return; + + ui->subProgressLabel->setText(QString()); + + /* make it skip to bandwidth stage if only set to config recording */ + if (stage == Stage::Starting) { + if (!started) { + blog(LOG_INFO, STARTING_SEPARATOR); + started = true; + } + + if (wiz->type != AutoConfig::Type::Streaming) { + stage = Stage::StreamEncoder; + } else if (!wiz->bandwidthTest) { + stage = Stage::BandwidthTest; + } + } + + if (stage == Stage::Starting) { + stage = Stage::BandwidthTest; + StartBandwidthStage(); + + } else if (stage == Stage::BandwidthTest) { + stage = Stage::StreamEncoder; + StartStreamEncoderStage(); + + } else if (stage == Stage::StreamEncoder) { + stage = Stage::RecordingEncoder; + StartRecordingEncoderStage(); + + } else { + stage = Stage::Finished; + FinalizeResults(); + emit completeChanged(); + } +} + +void AutoConfigTestPage::UpdateMessage(QString message) +{ + ui->subProgressLabel->setText(message); +} + +void AutoConfigTestPage::Failure(QString message) +{ + ui->errorLabel->setText(message); + ui->stackedWidget->setCurrentIndex(2); +} + +void AutoConfigTestPage::Progress(int percentage) +{ + ui->progressBar->setValue(percentage); +} + +AutoConfigTestPage::AutoConfigTestPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigTestPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.TestPage")); + setSubTitle(QTStr(SUBTITLE_TESTING)); + setCommitPage(true); +} + +AutoConfigTestPage::~AutoConfigTestPage() +{ + if (testThread.joinable()) { + { + unique_lock ul(m); + cancel = true; + cv.notify_one(); + } + testThread.join(); + } + + if (started) + blog(LOG_INFO, STOPPING_SEPARATOR); +} + +void AutoConfigTestPage::initializePage() +{ + if (wiz->type == AutoConfig::Type::VirtualCam) { + wiz->idealResolutionCX = wiz->baseResolutionCX; + wiz->idealResolutionCY = wiz->baseResolutionCY; + wiz->idealFPSNum = 30; + wiz->idealFPSDen = 1; + stage = Stage::Finished; + } else { + stage = Stage::Starting; + } + + setSubTitle(QTStr(SUBTITLE_TESTING)); + softwareTested = false; + cancel = false; + DeleteLayout(results); + results = new QFormLayout(); + results->setContentsMargins(0, 0, 0, 0); + ui->finishPageLayout->insertLayout(1, results); + ui->stackedWidget->setCurrentIndex(0); + NextStage(); +} + +void AutoConfigTestPage::cleanupPage() +{ + if (testThread.joinable()) { + { + unique_lock ul(m); + cancel = true; + cv.notify_one(); + } + testThread.join(); + } +} + +bool AutoConfigTestPage::isComplete() const +{ + return stage == Stage::Finished; +} + +int AutoConfigTestPage::nextId() const +{ + return -1; +} From e1fade1fc6906ca7f7b27c119f5b7ab43b513add Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 19:00:06 +0100 Subject: [PATCH 25/37] frontend: Split Qt UI autoconfig implementations into single files --- frontend/wizards/AutoConfig.cpp | 899 +-------------- frontend/wizards/AutoConfig.hpp | 164 --- frontend/wizards/AutoConfigStartPage.cpp | 1200 +------------------- frontend/wizards/AutoConfigStartPage.hpp | 264 ----- frontend/wizards/AutoConfigStreamPage.cpp | 573 +--------- frontend/wizards/AutoConfigStreamPage.hpp | 239 +--- frontend/wizards/AutoConfigTestPage.cpp | 97 +- frontend/wizards/AutoConfigVideoPage.cpp | 1128 +------------------ frontend/wizards/AutoConfigVideoPage.hpp | 268 ----- frontend/wizards/TestMode.hpp | 1203 +-------------------- 10 files changed, 63 insertions(+), 5972 deletions(-) diff --git a/frontend/wizards/AutoConfig.cpp b/frontend/wizards/AutoConfig.cpp index 6b1667609..90bfe0f65 100644 --- a/frontend/wizards/AutoConfig.cpp +++ b/frontend/wizards/AutoConfig.cpp @@ -1,46 +1,29 @@ -#include -#include +#include "AutoConfig.hpp" +#include "AutoConfigStartPage.hpp" +#include "AutoConfigStreamPage.hpp" +#include "AutoConfigTestPage.hpp" +#include "AutoConfigVideoPage.hpp" +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigStreamPage.h" +#include "ui_AutoConfigVideoPage.h" + +#ifdef YOUTUBE_ENABLED +#include +#include +#endif +#include -#include #include -#include - -#include "moc_window-basic-auto-config.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "url-push-button.hpp" - -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" - -#include "ui_AutoConfigStartPage.h" -#include "ui_AutoConfigVideoPage.h" -#include "ui_AutoConfigStreamPage.h" - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" -#include "ui-config.h" -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; - -#define wiz reinterpret_cast(wizard()) - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfig.cpp" constexpr std::string_view OBSServiceFileName = "service.json"; +enum class ListOpt : int { + ShowAll = 1, + Custom, +}; + static OBSData OpenServiceSettings(std::string &type) { const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); @@ -71,848 +54,6 @@ static void GetServiceInfo(std::string &type, std::string &service, std::string key = obs_data_get_string(settings, "key"); } -/* ------------------------------------------------------------------------- */ - -AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) -{ - ui->setupUi(this); - setTitle(QTStr("Basic.AutoConfig.StartPage")); - setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); - - OBSBasic *main = OBSBasic::Get(); - if (main->VCamEnabled()) { - QRadioButton *prioritizeVCam = - new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); - QBoxLayout *box = reinterpret_cast(layout()); - box->insertWidget(2, prioritizeVCam); - - connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); - } -} - -AutoConfigStartPage::~AutoConfigStartPage() {} - -int AutoConfigStartPage::nextId() const -{ - return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; -} - -void AutoConfigStartPage::on_prioritizeStreaming_clicked() -{ - wiz->type = AutoConfig::Type::Streaming; -} - -void AutoConfigStartPage::on_prioritizeRecording_clicked() -{ - wiz->type = AutoConfig::Type::Recording; -} - -void AutoConfigStartPage::PrioritizeVCam() -{ - wiz->type = AutoConfig::Type::VirtualCam; -} - -/* ------------------------------------------------------------------------- */ - -#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x -#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") -#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") -#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") -#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") -#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") - -AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) -{ - ui->setupUi(this); - - setTitle(QTStr("Basic.AutoConfig.VideoPage")); - setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; - - QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); - - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); - ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); - ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); - ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); - ui->fps->setCurrentIndex(0); - - QString cxStr = QString::number(ovi.base_width); - QString cyStr = QString::number(ovi.base_height); - - int encRes = int(ovi.base_width << 16) | int(ovi.base_height); - - // Auto config only supports testing down to 240p, don't allow current - // resolution if it's lower than that. - if (ovi.base_height >= 240) - ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); - - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QSize as = screen->size(); - int as_width = as.width(); - int as_height = as.height(); - - // Calculate physical screen resolution based on the virtual screen resolution - // They might differ if scaling is enabled, e.g. for HiDPI screens - as_width = round(as_width * screen->devicePixelRatio()); - as_height = round(as_height * screen->devicePixelRatio()); - - encRes = as_width << 16 | as_height; - - QString str = - QTStr(RES_USE_DISPLAY) - .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); - - ui->canvasRes->addItem(str, encRes); - } - - auto addRes = [&](int cx, int cy) { - encRes = (cx << 16) | cy; - QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); - ui->canvasRes->addItem(str, encRes); - }; - - addRes(1920, 1080); - addRes(1280, 720); - - ui->canvasRes->setCurrentIndex(0); -} - -AutoConfigVideoPage::~AutoConfigVideoPage() {} - -int AutoConfigVideoPage::nextId() const -{ - return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; -} - -bool AutoConfigVideoPage::validatePage() -{ - int encRes = ui->canvasRes->currentData().toInt(); - wiz->baseResolutionCX = encRes >> 16; - wiz->baseResolutionCY = encRes & 0xFFFF; - wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - switch (wiz->fpsType) { - case AutoConfig::FPSType::PreferHighFPS: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = true; - break; - case AutoConfig::FPSType::PreferHighRes: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::UseCurrent: - wiz->specificFPSNum = ovi.fps_num; - wiz->specificFPSDen = ovi.fps_den; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps30: - wiz->specificFPSNum = 30; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps60: - wiz->specificFPSNum = 60; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - } - - return true; -} - -/* ------------------------------------------------------------------------- */ - -enum class ListOpt : int { - ShowAll = 1, - Custom, -}; - -AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) -{ - ui->setupUi(this); - ui->bitrateLabel->setVisible(false); - ui->bitrate->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - ui->useMultitrackVideo->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - int vertSpacing = ui->topLayout->verticalSpacing(); - - QMargins m = ui->topLayout->contentsMargins(); - m.setBottom(vertSpacing / 2); - ui->topLayout->setContentsMargins(m); - - m = ui->loginPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->loginPageLayout->setContentsMargins(m); - - m = ui->streamkeyPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); - - setTitle(QTStr("Basic.AutoConfig.StreamPage")); - setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); - - LoadServices(false); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); - - connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - }); - - connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); -} - -AutoConfigStreamPage::~AutoConfigStreamPage() {} - -bool AutoConfigStreamPage::isComplete() const -{ - return ready; -} - -int AutoConfigStreamPage::nextId() const -{ - return AutoConfig::TestPage; -} - -inline bool AutoConfigStreamPage::IsCustomService() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::Custom; -} - -bool AutoConfigStreamPage::validatePage() -{ - OBSDataAutoRelease service_settings = obs_data_create(); - - wiz->customServer = IsCustomService(); - - const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; - - if (!wiz->customServer) { - obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); - } - - OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); - - int bitrate; - if (!ui->doBandwidthTest->isChecked()) { - bitrate = ui->bitrate->value(); - wiz->idealBitrate = bitrate; - } else { - /* Default test target is 10 Mbps */ - bitrate = 10000; -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(wiz->serviceName)) { - /* Adjust upper bound to YouTube limits - * for resolutions above 1080p */ - if (wiz->baseResolutionCY > 1440) - bitrate = 51000; - else if (wiz->baseResolutionCY > 1080) - bitrate = 18000; - } -#endif - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(service, settings, nullptr); - - if (wiz->customServer) { - QString server = ui->customServer->text().trimmed(); - wiz->server = wiz->serverName = QT_TO_UTF8(server); - } else { - wiz->serverName = QT_TO_UTF8(ui->server->currentText()); - wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); - } - - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); - wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); - wiz->idealBitrate = wiz->startingBitrate; - wiz->regionUS = ui->regionUS->isChecked(); - wiz->regionEU = ui->regionEU->isChecked(); - wiz->regionAsia = ui->regionAsia->isChecked(); - wiz->regionOther = ui->regionOther->isChecked(); - wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); - if (ui->preferHardware) - wiz->preferHardware = ui->preferHardware->isChecked(); - wiz->key = QT_TO_UTF8(ui->key->text()); - - if (!wiz->customServer) { - if (wiz->serviceName == "Twitch") - wiz->service = AutoConfig::Service::Twitch; -#ifdef YOUTUBE_ENABLED - else if (IsYouTubeService(wiz->serviceName)) - wiz->service = AutoConfig::Service::YouTube; -#endif - else if (wiz->serviceName == "Amazon IVS") - wiz->service = AutoConfig::Service::AmazonIVS; - else - wiz->service = AutoConfig::Service::Other; - } else { - wiz->service = AutoConfig::Service::Other; - } - - if (wiz->service == AutoConfig::Service::Twitch) { - wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); - - if (wiz->testMultitrackVideo) { - auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, - std::nullopt, false); - - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - try { - auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), - postData, multitrack_video_name); - - for (const auto &endpoint : config.ingest_endpoints) { - if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) - continue; - - std::string address = endpoint.url_template; - auto pos = address.find("/{stream_key}"); - if (pos != address.npos) - address.erase(pos); - - wiz->serviceConfigServers.push_back({address, address}); - } - - int multitrackVideoBitrate = 0; - for (auto &encoder_config : config.encoder_configurations) { - auto it = encoder_config.settings.find("bitrate"); - if (it == encoder_config.settings.end()) - continue; - - if (!it->is_number_integer()) - continue; - - int bitrate = 0; - it->get_to(bitrate); - multitrackVideoBitrate += bitrate; - } - - if (multitrackVideoBitrate > 0) { - wiz->startingBitrate = multitrackVideoBitrate; - wiz->idealBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.testSuccessful = true; - } - } catch (const MultitrackVideoError & /*err*/) { - // FIXME: do something sensible - } - } - } - - if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && - wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { - QMessageBox::StandardButton button; -#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) - button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); -#undef WARNING_TEXT - - if (button == QMessageBox::No) - return false; - } - - return true; -} - -void AutoConfigStreamPage::on_show_clicked() -{ - if (ui->key->echoMode() == QLineEdit::Password) { - ui->key->setEchoMode(QLineEdit::Normal); - ui->show->setText(QTStr("Hide")); - } else { - ui->key->setEchoMode(QLineEdit::Password); - ui->show->setText(QTStr("Show")); - } -} - -void AutoConfigStreamPage::OnOAuthStreamKeyConnected() -{ - OAuthStreamKey *a = reinterpret_cast(auth.get()); - - if (a) { - bool validKey = !a->key().empty(); - - if (validKey) - ui->key->setText(QT_UTF8(a->key().c_str())); - - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(a->service())) { - ui->key->clear(); - - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - - ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); - - YoutubeApiWrappers *ytAuth = reinterpret_cast(a); - ChannelDescription cd; - if (ytAuth->GetChannelDescription(cd)) { - ui->connectedAccountText->setText(cd.title); - - /* Create throwaway stream key for bandwidth test */ - if (ui->doBandwidthTest->isChecked()) { - StreamDescription stream = {"", "", "OBS Studio Test Stream"}; - if (ytAuth->InsertStream(stream)) { - ui->key->setText(stream.name); - } - } - } - } -#endif - } - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::OnAuthConnected() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - Auth::Type type = Auth::AuthType(service); - - if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { - OnOAuthStreamKeyConnected(); - } -} - -void AutoConfigStreamPage::on_connectAccount_clicked() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - - OAuth::DeleteCookies(service); - - auth = OAuthStreamKey::Login(this, service); - if (!!auth) { - OnAuthConnected(); - - ui->useStreamKeyAdv->setVisible(false); - } -} - -#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" -#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" - -void AutoConfigStreamPage::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { - return; - } - - OBSBasic *main = OBSBasic::Get(); - - main->auth.reset(); - auth.reset(); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif - - reset_service_ui_fields(service); - - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - /* Restore key link when disconnecting account */ - UpdateKeyLink(); -} - -void AutoConfigStreamPage::on_useStreamKey_clicked() -{ - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::on_preferHardware_clicked() -{ - auto *main = OBSBasic::Get(); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - - ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); - ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); - ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); -} - -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - -static inline bool is_external_oauth(const std::string &service) -{ - return Auth::External(service); -} - -void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) -{ -#ifdef YOUTUBE_ENABLED - // when account is already connected: - OAuthStreamKey *a = reinterpret_cast(auth.get()); - if (a && service == a->service() && IsYouTubeService(a->service())) { - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - return; - } -#endif - - bool external_oauth = is_external_oauth(service); - if (external_oauth) { - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(true); - ui->useStreamKeyAdv->setVisible(true); - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; - - ui->stackedWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - ui->useStreamKeyAdv->setVisible(false); - } else { - ui->connectAccount2->setVisible(false); - ui->useStreamKeyAdv->setVisible(false); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} - -void AutoConfigStreamPage::ServiceChanged() -{ - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - if (showMore) - return; - - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool regionBased = service == "Twitch"; - bool testBandwidth = ui->doBandwidthTest->isChecked(); - bool custom = IsCustomService(); - - bool ertmp_multitrack_video_available = service == "Twitch"; - - bool custom_disclaimer = false; - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (!custom) { - OBSDataAutoRelease service_settings = obs_data_create(); - obs_data_set_string(service_settings, "service", service.c_str()); - OBSServiceAutoRelease obs_service = - obs_service_create("rtmp_common", "temp service", service_settings, nullptr); - - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { - ui->multitrackVideoInfo->setText( - obs_data_get_string(service_settings, "multitrack_video_disclaimer")); - custom_disclaimer = true; - } - } - - if (!custom_disclaimer) { - ui->multitrackVideoInfo->setText( - QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); - } - - ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setText( - QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); - ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); - ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); - - reset_service_ui_fields(service); - - /* Test three closest servers if "Auto" is available for Twitch */ - if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) - regionBased = false; - - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); - ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); - - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(false); - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - } else { - if (!testBandwidth) - ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(regionBased && testBandwidth); - ui->serverStackedWidget->setCurrentIndex(0); - ui->serverStackedWidget->setHidden(testBandwidth); - ui->serverLabel->setHidden(testBandwidth); - } - - wiz->testRegions = regionBased && testBandwidth; - - ui->bitrateLabel->setHidden(testBandwidth); - ui->bitrate->setHidden(testBandwidth); - - OBSBasic *main = OBSBasic::Get(); - - if (main->auth) { - auto system_auth_service = main->auth->service(); - bool service_check = service.find(system_auth_service) != std::string::npos; -#ifdef YOUTUBE_ENABLED - service_check = service_check ? service_check - : IsYouTubeService(system_auth_service) && IsYouTubeService(service); -#endif - if (service_check) { - auth.reset(); - auth = main->auth; - OnAuthConnected(); - } - } - - UpdateCompleted(); -} - -void AutoConfigStreamPage::UpdateMoreInfoLink() -{ - if (IsCustomService()) { - ui->moreInfoButton->hide(); - return; - } - - QString serviceName = ui->service->currentText(); - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - const char *more_info_link = obs_data_get_string(settings, "more_info_link"); - - if (!more_info_link || (*more_info_link == '\0')) { - ui->moreInfoButton->hide(); - } else { - ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); - ui->moreInfoButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateKeyLink() -{ - QString serviceName = ui->service->currentText(); - QString customServer = ui->customServer->text().trimmed(); - QString streamKeyLink; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); - - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; - } - - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - ui->streamKeyLabel->setToolTip(""); - } else if (!IsCustomService()) { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - ui->streamKeyLabel->setToolTip(""); - } else { - /* add tooltips for stream key */ - QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; - QString lStr = "%1 "; - - ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); - ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); - } - - if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { - ui->streamKeyButton->hide(); - } else { - ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->streamKeyButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_bool(settings, "show_all", showAll); - - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); - - ui->service->blockSignals(true); - ui->service->clear(); - - QStringList names; - - obs_property_t *services = obs_properties_get(props, "service"); - size_t services_count = obs_property_list_item_count(services); - for (size_t i = 0; i < services_count; i++) { - const char *name = obs_property_list_item_string(services, i); - names.push_back(name); - } - - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { - ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); - } - - ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); - - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); - } - - obs_properties_destroy(props); - - ui->service->blockSignals(false); -} - -void AutoConfigStreamPage::UpdateServerList() -{ - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - - if (showMore) { - LoadServices(true); - ui->service->showPopup(); - return; - } else { - lastService = serviceName; - } - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - obs_property_t *servers = obs_properties_get(props, "server"); - - ui->server->clear(); - - size_t servers_count = obs_property_list_item_count(servers); - for (size_t i = 0; i < servers_count; i++) { - const char *name = obs_property_list_item_name(servers, i); - const char *server = obs_property_list_item_string(servers, i); - ui->server->addItem(name, server); - } - - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateCompleted() -{ - const bool custom = IsCustomService(); - if (ui->stackedWidget->currentIndex() == (int)Section::Connect || - (ui->key->text().isEmpty() && !auth && !custom)) { - ready = false; - } else { - if (custom) { - ready = !ui->customServer->text().isEmpty(); - } else { - ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || - ui->regionAsia->isChecked() || ui->regionOther->isChecked(); - } - } - emit completeChanged(); -} - -/* ------------------------------------------------------------------------- */ - AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) { EnableThreadedMessageBoxes(true); diff --git a/frontend/wizards/AutoConfig.hpp b/frontend/wizards/AutoConfig.hpp index 7e31bdf89..92fa568f9 100644 --- a/frontend/wizards/AutoConfig.hpp +++ b/frontend/wizards/AutoConfig.hpp @@ -1,26 +1,8 @@ #pragma once #include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -class Ui_AutoConfigStartPage; -class Ui_AutoConfigVideoPage; -class Ui_AutoConfigStreamPage; -class Ui_AutoConfigTestPage; class AutoConfigStreamPage; -class Auth; class AutoConfig : public QWizard { Q_OBJECT @@ -140,149 +122,3 @@ public: TestPage, }; }; - -class AutoConfigStartPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigStartPage(QWidget *parent = nullptr); - ~AutoConfigStartPage(); - - virtual int nextId() const override; - -public slots: - void on_prioritizeStreaming_clicked(); - void on_prioritizeRecording_clicked(); - void PrioritizeVCam(); -}; - -class AutoConfigVideoPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigVideoPage(QWidget *parent = nullptr); - ~AutoConfigVideoPage(); - - virtual int nextId() const override; - virtual bool validatePage() override; -}; - -class AutoConfigStreamPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - - std::unique_ptr ui; - QString lastService; - bool ready = false; - - void LoadServices(bool showAll); - inline bool IsCustomService() const; - -public: - AutoConfigStreamPage(QWidget *parent = nullptr); - ~AutoConfigStreamPage(); - - virtual bool isComplete() const override; - virtual int nextId() const override; - virtual bool validatePage() override; - - void OnAuthConnected(); - void OnOAuthStreamKeyConnected(); - -public slots: - void on_show_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_preferHardware_clicked(); - void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); - void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); -}; - -class AutoConfigTestPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - QPointer results; - - std::unique_ptr ui; - std::thread testThread; - std::condition_variable cv; - std::mutex m; - bool cancel = false; - bool started = false; - - enum class Stage { - Starting, - BandwidthTest, - StreamEncoder, - RecordingEncoder, - Finished, - }; - - Stage stage = Stage::Starting; - bool softwareTested = false; - - void StartBandwidthStage(); - void StartStreamEncoderStage(); - void StartRecordingEncoderStage(); - - void FindIdealHardwareResolution(); - bool TestSoftwareEncoding(); - - void TestBandwidthThread(); - void TestStreamEncoderThread(); - void TestRecordingEncoderThread(); - - void FinalizeResults(); - - struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} - }; - - void GetServers(std::vector &servers); - -public: - AutoConfigTestPage(QWidget *parent = nullptr); - ~AutoConfigTestPage(); - - virtual void initializePage() override; - virtual void cleanupPage() override; - virtual bool isComplete() const override; - virtual int nextId() const override; - -public slots: - void NextStage(); - void UpdateMessage(QString message); - void Failure(QString message); - void Progress(int percentage); -}; diff --git a/frontend/wizards/AutoConfigStartPage.cpp b/frontend/wizards/AutoConfigStartPage.cpp index 6b1667609..706123a6f 100644 --- a/frontend/wizards/AutoConfigStartPage.cpp +++ b/frontend/wizards/AutoConfigStartPage.cpp @@ -1,78 +1,13 @@ -#include -#include - -#include -#include - -#include - -#include "moc_window-basic-auto-config.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "url-push-button.hpp" - -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" - +#include "AutoConfigStartPage.hpp" +#include "AutoConfig.hpp" #include "ui_AutoConfigStartPage.h" -#include "ui_AutoConfigVideoPage.h" -#include "ui_AutoConfigStreamPage.h" -#ifdef BROWSER_AVAILABLE -#include -#endif +#include -#include "auth-oauth.hpp" -#include "ui-config.h" -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; +#include "moc_AutoConfigStartPage.cpp" #define wiz reinterpret_cast(wizard()) -/* ------------------------------------------------------------------------- */ - -constexpr std::string_view OBSServiceFileName = "service.json"; - -static OBSData OpenServiceSettings(std::string &type) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - if (!std::filesystem::exists(jsonFilePath)) { - return OBSData(); - } - - OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - - return settings.Get(); -} - -static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) -{ - OBSData settings = OpenServiceSettings(type); - - service = obs_data_get_string(settings, "service"); - server = obs_data_get_string(settings, "server"); - key = obs_data_get_string(settings, "key"); -} - -/* ------------------------------------------------------------------------- */ - AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) { ui->setupUi(this); @@ -111,1130 +46,3 @@ void AutoConfigStartPage::PrioritizeVCam() { wiz->type = AutoConfig::Type::VirtualCam; } - -/* ------------------------------------------------------------------------- */ - -#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x -#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") -#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") -#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") -#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") -#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") - -AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) -{ - ui->setupUi(this); - - setTitle(QTStr("Basic.AutoConfig.VideoPage")); - setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; - - QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); - - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); - ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); - ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); - ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); - ui->fps->setCurrentIndex(0); - - QString cxStr = QString::number(ovi.base_width); - QString cyStr = QString::number(ovi.base_height); - - int encRes = int(ovi.base_width << 16) | int(ovi.base_height); - - // Auto config only supports testing down to 240p, don't allow current - // resolution if it's lower than that. - if (ovi.base_height >= 240) - ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); - - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QSize as = screen->size(); - int as_width = as.width(); - int as_height = as.height(); - - // Calculate physical screen resolution based on the virtual screen resolution - // They might differ if scaling is enabled, e.g. for HiDPI screens - as_width = round(as_width * screen->devicePixelRatio()); - as_height = round(as_height * screen->devicePixelRatio()); - - encRes = as_width << 16 | as_height; - - QString str = - QTStr(RES_USE_DISPLAY) - .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); - - ui->canvasRes->addItem(str, encRes); - } - - auto addRes = [&](int cx, int cy) { - encRes = (cx << 16) | cy; - QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); - ui->canvasRes->addItem(str, encRes); - }; - - addRes(1920, 1080); - addRes(1280, 720); - - ui->canvasRes->setCurrentIndex(0); -} - -AutoConfigVideoPage::~AutoConfigVideoPage() {} - -int AutoConfigVideoPage::nextId() const -{ - return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; -} - -bool AutoConfigVideoPage::validatePage() -{ - int encRes = ui->canvasRes->currentData().toInt(); - wiz->baseResolutionCX = encRes >> 16; - wiz->baseResolutionCY = encRes & 0xFFFF; - wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - switch (wiz->fpsType) { - case AutoConfig::FPSType::PreferHighFPS: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = true; - break; - case AutoConfig::FPSType::PreferHighRes: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::UseCurrent: - wiz->specificFPSNum = ovi.fps_num; - wiz->specificFPSDen = ovi.fps_den; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps30: - wiz->specificFPSNum = 30; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps60: - wiz->specificFPSNum = 60; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - } - - return true; -} - -/* ------------------------------------------------------------------------- */ - -enum class ListOpt : int { - ShowAll = 1, - Custom, -}; - -AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) -{ - ui->setupUi(this); - ui->bitrateLabel->setVisible(false); - ui->bitrate->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - ui->useMultitrackVideo->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - int vertSpacing = ui->topLayout->verticalSpacing(); - - QMargins m = ui->topLayout->contentsMargins(); - m.setBottom(vertSpacing / 2); - ui->topLayout->setContentsMargins(m); - - m = ui->loginPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->loginPageLayout->setContentsMargins(m); - - m = ui->streamkeyPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); - - setTitle(QTStr("Basic.AutoConfig.StreamPage")); - setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); - - LoadServices(false); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); - - connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - }); - - connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); -} - -AutoConfigStreamPage::~AutoConfigStreamPage() {} - -bool AutoConfigStreamPage::isComplete() const -{ - return ready; -} - -int AutoConfigStreamPage::nextId() const -{ - return AutoConfig::TestPage; -} - -inline bool AutoConfigStreamPage::IsCustomService() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::Custom; -} - -bool AutoConfigStreamPage::validatePage() -{ - OBSDataAutoRelease service_settings = obs_data_create(); - - wiz->customServer = IsCustomService(); - - const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; - - if (!wiz->customServer) { - obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); - } - - OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); - - int bitrate; - if (!ui->doBandwidthTest->isChecked()) { - bitrate = ui->bitrate->value(); - wiz->idealBitrate = bitrate; - } else { - /* Default test target is 10 Mbps */ - bitrate = 10000; -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(wiz->serviceName)) { - /* Adjust upper bound to YouTube limits - * for resolutions above 1080p */ - if (wiz->baseResolutionCY > 1440) - bitrate = 51000; - else if (wiz->baseResolutionCY > 1080) - bitrate = 18000; - } -#endif - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(service, settings, nullptr); - - if (wiz->customServer) { - QString server = ui->customServer->text().trimmed(); - wiz->server = wiz->serverName = QT_TO_UTF8(server); - } else { - wiz->serverName = QT_TO_UTF8(ui->server->currentText()); - wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); - } - - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); - wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); - wiz->idealBitrate = wiz->startingBitrate; - wiz->regionUS = ui->regionUS->isChecked(); - wiz->regionEU = ui->regionEU->isChecked(); - wiz->regionAsia = ui->regionAsia->isChecked(); - wiz->regionOther = ui->regionOther->isChecked(); - wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); - if (ui->preferHardware) - wiz->preferHardware = ui->preferHardware->isChecked(); - wiz->key = QT_TO_UTF8(ui->key->text()); - - if (!wiz->customServer) { - if (wiz->serviceName == "Twitch") - wiz->service = AutoConfig::Service::Twitch; -#ifdef YOUTUBE_ENABLED - else if (IsYouTubeService(wiz->serviceName)) - wiz->service = AutoConfig::Service::YouTube; -#endif - else if (wiz->serviceName == "Amazon IVS") - wiz->service = AutoConfig::Service::AmazonIVS; - else - wiz->service = AutoConfig::Service::Other; - } else { - wiz->service = AutoConfig::Service::Other; - } - - if (wiz->service == AutoConfig::Service::Twitch) { - wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); - - if (wiz->testMultitrackVideo) { - auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, - std::nullopt, false); - - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - try { - auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), - postData, multitrack_video_name); - - for (const auto &endpoint : config.ingest_endpoints) { - if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) - continue; - - std::string address = endpoint.url_template; - auto pos = address.find("/{stream_key}"); - if (pos != address.npos) - address.erase(pos); - - wiz->serviceConfigServers.push_back({address, address}); - } - - int multitrackVideoBitrate = 0; - for (auto &encoder_config : config.encoder_configurations) { - auto it = encoder_config.settings.find("bitrate"); - if (it == encoder_config.settings.end()) - continue; - - if (!it->is_number_integer()) - continue; - - int bitrate = 0; - it->get_to(bitrate); - multitrackVideoBitrate += bitrate; - } - - if (multitrackVideoBitrate > 0) { - wiz->startingBitrate = multitrackVideoBitrate; - wiz->idealBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.testSuccessful = true; - } - } catch (const MultitrackVideoError & /*err*/) { - // FIXME: do something sensible - } - } - } - - if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && - wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { - QMessageBox::StandardButton button; -#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) - button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); -#undef WARNING_TEXT - - if (button == QMessageBox::No) - return false; - } - - return true; -} - -void AutoConfigStreamPage::on_show_clicked() -{ - if (ui->key->echoMode() == QLineEdit::Password) { - ui->key->setEchoMode(QLineEdit::Normal); - ui->show->setText(QTStr("Hide")); - } else { - ui->key->setEchoMode(QLineEdit::Password); - ui->show->setText(QTStr("Show")); - } -} - -void AutoConfigStreamPage::OnOAuthStreamKeyConnected() -{ - OAuthStreamKey *a = reinterpret_cast(auth.get()); - - if (a) { - bool validKey = !a->key().empty(); - - if (validKey) - ui->key->setText(QT_UTF8(a->key().c_str())); - - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(a->service())) { - ui->key->clear(); - - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - - ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); - - YoutubeApiWrappers *ytAuth = reinterpret_cast(a); - ChannelDescription cd; - if (ytAuth->GetChannelDescription(cd)) { - ui->connectedAccountText->setText(cd.title); - - /* Create throwaway stream key for bandwidth test */ - if (ui->doBandwidthTest->isChecked()) { - StreamDescription stream = {"", "", "OBS Studio Test Stream"}; - if (ytAuth->InsertStream(stream)) { - ui->key->setText(stream.name); - } - } - } - } -#endif - } - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::OnAuthConnected() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - Auth::Type type = Auth::AuthType(service); - - if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { - OnOAuthStreamKeyConnected(); - } -} - -void AutoConfigStreamPage::on_connectAccount_clicked() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - - OAuth::DeleteCookies(service); - - auth = OAuthStreamKey::Login(this, service); - if (!!auth) { - OnAuthConnected(); - - ui->useStreamKeyAdv->setVisible(false); - } -} - -#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" -#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" - -void AutoConfigStreamPage::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { - return; - } - - OBSBasic *main = OBSBasic::Get(); - - main->auth.reset(); - auth.reset(); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif - - reset_service_ui_fields(service); - - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - /* Restore key link when disconnecting account */ - UpdateKeyLink(); -} - -void AutoConfigStreamPage::on_useStreamKey_clicked() -{ - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::on_preferHardware_clicked() -{ - auto *main = OBSBasic::Get(); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - - ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); - ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); - ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); -} - -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - -static inline bool is_external_oauth(const std::string &service) -{ - return Auth::External(service); -} - -void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) -{ -#ifdef YOUTUBE_ENABLED - // when account is already connected: - OAuthStreamKey *a = reinterpret_cast(auth.get()); - if (a && service == a->service() && IsYouTubeService(a->service())) { - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - return; - } -#endif - - bool external_oauth = is_external_oauth(service); - if (external_oauth) { - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(true); - ui->useStreamKeyAdv->setVisible(true); - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; - - ui->stackedWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - ui->useStreamKeyAdv->setVisible(false); - } else { - ui->connectAccount2->setVisible(false); - ui->useStreamKeyAdv->setVisible(false); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} - -void AutoConfigStreamPage::ServiceChanged() -{ - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - if (showMore) - return; - - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool regionBased = service == "Twitch"; - bool testBandwidth = ui->doBandwidthTest->isChecked(); - bool custom = IsCustomService(); - - bool ertmp_multitrack_video_available = service == "Twitch"; - - bool custom_disclaimer = false; - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (!custom) { - OBSDataAutoRelease service_settings = obs_data_create(); - obs_data_set_string(service_settings, "service", service.c_str()); - OBSServiceAutoRelease obs_service = - obs_service_create("rtmp_common", "temp service", service_settings, nullptr); - - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { - ui->multitrackVideoInfo->setText( - obs_data_get_string(service_settings, "multitrack_video_disclaimer")); - custom_disclaimer = true; - } - } - - if (!custom_disclaimer) { - ui->multitrackVideoInfo->setText( - QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); - } - - ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setText( - QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); - ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); - ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); - - reset_service_ui_fields(service); - - /* Test three closest servers if "Auto" is available for Twitch */ - if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) - regionBased = false; - - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); - ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); - - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(false); - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - } else { - if (!testBandwidth) - ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(regionBased && testBandwidth); - ui->serverStackedWidget->setCurrentIndex(0); - ui->serverStackedWidget->setHidden(testBandwidth); - ui->serverLabel->setHidden(testBandwidth); - } - - wiz->testRegions = regionBased && testBandwidth; - - ui->bitrateLabel->setHidden(testBandwidth); - ui->bitrate->setHidden(testBandwidth); - - OBSBasic *main = OBSBasic::Get(); - - if (main->auth) { - auto system_auth_service = main->auth->service(); - bool service_check = service.find(system_auth_service) != std::string::npos; -#ifdef YOUTUBE_ENABLED - service_check = service_check ? service_check - : IsYouTubeService(system_auth_service) && IsYouTubeService(service); -#endif - if (service_check) { - auth.reset(); - auth = main->auth; - OnAuthConnected(); - } - } - - UpdateCompleted(); -} - -void AutoConfigStreamPage::UpdateMoreInfoLink() -{ - if (IsCustomService()) { - ui->moreInfoButton->hide(); - return; - } - - QString serviceName = ui->service->currentText(); - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - const char *more_info_link = obs_data_get_string(settings, "more_info_link"); - - if (!more_info_link || (*more_info_link == '\0')) { - ui->moreInfoButton->hide(); - } else { - ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); - ui->moreInfoButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateKeyLink() -{ - QString serviceName = ui->service->currentText(); - QString customServer = ui->customServer->text().trimmed(); - QString streamKeyLink; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); - - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; - } - - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - ui->streamKeyLabel->setToolTip(""); - } else if (!IsCustomService()) { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - ui->streamKeyLabel->setToolTip(""); - } else { - /* add tooltips for stream key */ - QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; - QString lStr = "%1 "; - - ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); - ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); - } - - if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { - ui->streamKeyButton->hide(); - } else { - ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->streamKeyButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_bool(settings, "show_all", showAll); - - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); - - ui->service->blockSignals(true); - ui->service->clear(); - - QStringList names; - - obs_property_t *services = obs_properties_get(props, "service"); - size_t services_count = obs_property_list_item_count(services); - for (size_t i = 0; i < services_count; i++) { - const char *name = obs_property_list_item_string(services, i); - names.push_back(name); - } - - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { - ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); - } - - ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); - - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); - } - - obs_properties_destroy(props); - - ui->service->blockSignals(false); -} - -void AutoConfigStreamPage::UpdateServerList() -{ - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - - if (showMore) { - LoadServices(true); - ui->service->showPopup(); - return; - } else { - lastService = serviceName; - } - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - obs_property_t *servers = obs_properties_get(props, "server"); - - ui->server->clear(); - - size_t servers_count = obs_property_list_item_count(servers); - for (size_t i = 0; i < servers_count; i++) { - const char *name = obs_property_list_item_name(servers, i); - const char *server = obs_property_list_item_string(servers, i); - ui->server->addItem(name, server); - } - - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateCompleted() -{ - const bool custom = IsCustomService(); - if (ui->stackedWidget->currentIndex() == (int)Section::Connect || - (ui->key->text().isEmpty() && !auth && !custom)) { - ready = false; - } else { - if (custom) { - ready = !ui->customServer->text().isEmpty(); - } else { - ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || - ui->regionAsia->isChecked() || ui->regionOther->isChecked(); - } - } - emit completeChanged(); -} - -/* ------------------------------------------------------------------------- */ - -AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) -{ - EnableThreadedMessageBoxes(true); - - calldata_t cd = {0}; - calldata_set_int(&cd, "seconds", 5); - - proc_handler_t *ph = obs_get_proc_handler(); - proc_handler_call(ph, "twitch_ingests_refresh", &cd); - proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); - calldata_free(&cd); - - OBSBasic *main = reinterpret_cast(parent); - main->EnableOutputs(false); - - installEventFilter(CreateShortcutFilter()); - - std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); -#if defined(_WIN32) || defined(__APPLE__) - setWizardStyle(QWizard::ModernStyle); -#endif - streamPage = new AutoConfigStreamPage(); - - setPage(StartPage, new AutoConfigStartPage()); - setPage(VideoPage, new AutoConfigVideoPage()); - setPage(StreamPage, streamPage); - setPage(TestPage, new AutoConfigTestPage()); - setWindowTitle(QTStr("Basic.AutoConfig")); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - baseResolutionCX = ovi.base_width; - baseResolutionCY = ovi.base_height; - - /* ----------------------------------------- */ - /* check to see if Twitch's "auto" available */ - - OBSDataAutoRelease twitchSettings = obs_data_create(); - - obs_data_set_string(twitchSettings, "service", "Twitch"); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, twitchSettings); - - obs_property_t *p = obs_properties_get(props, "server"); - const char *first = obs_property_list_item_string(p, 0); - twitchAuto = strcmp(first, "auto") == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* check to see if Amazon IVS "auto" entries are available */ - - OBSDataAutoRelease amazonIVSSettings = obs_data_create(); - - obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); - - props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, amazonIVSSettings); - - p = obs_properties_get(props, "server"); - first = obs_property_list_item_string(p, 0); - amazonIVSAuto = strncmp(first, "auto", 4) == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* load service/servers */ - - customServer = serviceType == "rtmp_custom"; - - QComboBox *serviceList = streamPage->ui->service; - - if (!serviceName.empty()) { - serviceList->blockSignals(true); - - int count = serviceList->count(); - bool found = false; - - for (int i = 0; i < count; i++) { - QString name = serviceList->itemText(i); - - if (name == serviceName.c_str()) { - serviceList->setCurrentIndex(i); - found = true; - break; - } - } - - if (!found) { - serviceList->insertItem(0, serviceName.c_str()); - serviceList->setCurrentIndex(0); - } - - serviceList->blockSignals(false); - } - - streamPage->UpdateServerList(); - streamPage->UpdateKeyLink(); - streamPage->UpdateMoreInfoLink(); - streamPage->lastService.clear(); - - if (!customServer) { - QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); - if (idx == -1) - idx = 0; - - serverList->setCurrentIndex(idx); - } else { - streamPage->ui->customServer->setText(server.c_str()); - int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); - streamPage->ui->service->setCurrentIndex(idx); - } - - if (!key.empty()) - streamPage->ui->key->setText(key.c_str()); - - TestHardwareEncoding(); - - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - streamPage->ui->bitrate->setValue(bitrate); - streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); - streamPage->ServiceChanged(); - - if (!hardwareEncodingAvailable) { - delete streamPage->ui->preferHardware; - streamPage->ui->preferHardware = nullptr; - } else { - /* Newer generations of NVENC have a high enough quality to - * bitrate ratio that if NVENC is available, it makes sense to - * just always prefer hardware encoding by default */ - bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; - streamPage->ui->preferHardware->setChecked(preferHardware); - } - - setOptions(QWizard::WizardOptions()); - setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); - setButtonText(QWizard::BackButton, QTStr("Back")); - setButtonText(QWizard::NextButton, QTStr("Next")); - setButtonText(QWizard::CancelButton, QTStr("Cancel")); -} - -AutoConfig::~AutoConfig() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - main->EnableOutputs(true); - EnableThreadedMessageBoxes(false); -} - -void AutoConfig::TestHardwareEncoding() -{ - size_t idx = 0; - const char *id; - while (obs_enum_encoder_types(idx++, &id)) { - if (strcmp(id, "ffmpeg_nvenc") == 0) - hardwareEncodingAvailable = nvencAvailable = true; - else if (strcmp(id, "obs_qsv11") == 0) - hardwareEncodingAvailable = qsvAvailable = true; - else if (strcmp(id, "h264_texture_amf") == 0) - hardwareEncodingAvailable = vceAvailable = true; -#ifdef __APPLE__ - else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - if (__builtin_available(macOS 13.0, *)) - hardwareEncodingAvailable = appleAvailable = true; -#endif - } -} - -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (service == Service::Twitch) { - if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionUS; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAsia; - } else if (regionOther) { - return true; - } - } else { - return true; - } - - return false; -} - -void AutoConfig::done(int result) -{ - QWizard::done(result); - - if (result == QDialog::Accepted) { - if (type == Type::Streaming) - SaveStreamSettings(); - SaveSettings(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) { - OBSBasic *main = OBSBasic::Get(); - main->NewYouTubeAppDock(); - } -#endif - } -} - -inline const char *AutoConfig::GetEncoderId(Encoder enc) -{ - switch (enc) { - case Encoder::NVENC: - return SIMPLE_ENCODER_NVENC; - case Encoder::QSV: - return SIMPLE_ENCODER_QSV; - case Encoder::AMD: - return SIMPLE_ENCODER_AMD; - case Encoder::Apple: - return SIMPLE_ENCODER_APPLE_H264; - default: - return SIMPLE_ENCODER_X264; - } -}; - -void AutoConfig::SaveStreamSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - /* ---------------------------------- */ - /* save service */ - - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); - - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!streamPage->auth || !IsYouTubeService(serviceName)) - obs_data_set_string(settings, "key", key.c_str()); -#else - obs_data_set_string(settings, "key", key.c_str()); -#endif - - OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); - - if (!newService) - return; - - main->SetService(newService); - main->SaveService(); - main->auth = streamPage->auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } - - /* ---------------------------------- */ - /* save stream settings */ - - config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); - config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); - config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); - - config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); - - if (multitrackVideo.targetBitrate.has_value()) - config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", - *multitrackVideo.targetBitrate); - else - config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); - - if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && - (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - } else if (multitrackVideo.bitrate.has_value()) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); - config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", - *multitrackVideo.bitrate); - } -} - -void AutoConfig::SaveSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (recordingEncoder != Encoder::Stream) - config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); - - const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; - - config_set_string(main->Config(), "Output", "Mode", "Simple"); - config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); - config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); - config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); - config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); - config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); - - if (fpsType != FPSType::UseCurrent) { - config_set_uint(main->Config(), "Video", "FPSType", 0); - config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); - } - - main->ResetVideo(); - main->ResetOutputs(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/frontend/wizards/AutoConfigStartPage.hpp b/frontend/wizards/AutoConfigStartPage.hpp index 7e31bdf89..73cff2838 100644 --- a/frontend/wizards/AutoConfigStartPage.hpp +++ b/frontend/wizards/AutoConfigStartPage.hpp @@ -1,145 +1,8 @@ #pragma once -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include - class Ui_AutoConfigStartPage; -class Ui_AutoConfigVideoPage; -class Ui_AutoConfigStreamPage; -class Ui_AutoConfigTestPage; - -class AutoConfigStreamPage; -class Auth; - -class AutoConfig : public QWizard { - Q_OBJECT - - friend class AutoConfigStartPage; - friend class AutoConfigVideoPage; - friend class AutoConfigStreamPage; - friend class AutoConfigTestPage; - - enum class Type { - Invalid, - Streaming, - Recording, - VirtualCam, - }; - - enum class Service { - Twitch, - YouTube, - AmazonIVS, - Other, - }; - - enum class Encoder { - x264, - NVENC, - QSV, - AMD, - Apple, - Stream, - }; - - enum class Quality { - Stream, - High, - }; - - enum class FPSType : int { - PreferHighFPS, - PreferHighRes, - UseCurrent, - fps30, - fps60, - }; - - struct StreamServer { - std::string name; - std::string address; - }; - - static inline const char *GetEncoderId(Encoder enc); - - AutoConfigStreamPage *streamPage = nullptr; - - Service service = Service::Other; - Quality recordingQuality = Quality::Stream; - Encoder recordingEncoder = Encoder::Stream; - Encoder streamingEncoder = Encoder::x264; - Type type = Type::Streaming; - FPSType fpsType = FPSType::PreferHighFPS; - int idealBitrate = 2500; - struct { - std::optional targetBitrate; - std::optional bitrate; - bool testSuccessful = false; - } multitrackVideo; - int baseResolutionCX = 1920; - int baseResolutionCY = 1080; - int idealResolutionCX = 1280; - int idealResolutionCY = 720; - int idealFPSNum = 60; - int idealFPSDen = 1; - std::string serviceName; - std::string serverName; - std::string server; - std::vector serviceConfigServers; - std::string key; - - bool hardwareEncodingAvailable = false; - bool nvencAvailable = false; - bool qsvAvailable = false; - bool vceAvailable = false; - bool appleAvailable = false; - - int startingBitrate = 2500; - bool customServer = false; - bool bandwidthTest = false; - bool testMultitrackVideo = false; - bool testRegions = true; - bool twitchAuto = false; - bool amazonIVSAuto = false; - bool regionUS = true; - bool regionEU = true; - bool regionAsia = true; - bool regionOther = true; - bool preferHighFPS = false; - bool preferHardware = false; - int specificFPSNum = 0; - int specificFPSDen = 0; - - void TestHardwareEncoding(); - bool CanTestServer(const char *server); - - virtual void done(int result) override; - - void SaveStreamSettings(); - void SaveSettings(); - -public: - AutoConfig(QWidget *parent); - ~AutoConfig(); - - enum Page { - StartPage, - VideoPage, - StreamPage, - TestPage, - }; -}; class AutoConfigStartPage : public QWizardPage { Q_OBJECT @@ -159,130 +22,3 @@ public slots: void on_prioritizeRecording_clicked(); void PrioritizeVCam(); }; - -class AutoConfigVideoPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigVideoPage(QWidget *parent = nullptr); - ~AutoConfigVideoPage(); - - virtual int nextId() const override; - virtual bool validatePage() override; -}; - -class AutoConfigStreamPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - - std::unique_ptr ui; - QString lastService; - bool ready = false; - - void LoadServices(bool showAll); - inline bool IsCustomService() const; - -public: - AutoConfigStreamPage(QWidget *parent = nullptr); - ~AutoConfigStreamPage(); - - virtual bool isComplete() const override; - virtual int nextId() const override; - virtual bool validatePage() override; - - void OnAuthConnected(); - void OnOAuthStreamKeyConnected(); - -public slots: - void on_show_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_preferHardware_clicked(); - void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); - void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); -}; - -class AutoConfigTestPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - QPointer results; - - std::unique_ptr ui; - std::thread testThread; - std::condition_variable cv; - std::mutex m; - bool cancel = false; - bool started = false; - - enum class Stage { - Starting, - BandwidthTest, - StreamEncoder, - RecordingEncoder, - Finished, - }; - - Stage stage = Stage::Starting; - bool softwareTested = false; - - void StartBandwidthStage(); - void StartStreamEncoderStage(); - void StartRecordingEncoderStage(); - - void FindIdealHardwareResolution(); - bool TestSoftwareEncoding(); - - void TestBandwidthThread(); - void TestStreamEncoderThread(); - void TestRecordingEncoderThread(); - - void FinalizeResults(); - - struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} - }; - - void GetServers(std::vector &servers); - -public: - AutoConfigTestPage(QWidget *parent = nullptr); - ~AutoConfigTestPage(); - - virtual void initializePage() override; - virtual void cleanupPage() override; - virtual bool isComplete() const override; - virtual int nextId() const override; - -public slots: - void NextStage(); - void UpdateMessage(QString message); - void Failure(QString message); - void Progress(int percentage); -}; diff --git a/frontend/wizards/AutoConfigStreamPage.cpp b/frontend/wizards/AutoConfigStreamPage.cpp index 6b1667609..86d11c3cf 100644 --- a/frontend/wizards/AutoConfigStreamPage.cpp +++ b/frontend/wizards/AutoConfigStreamPage.cpp @@ -1,245 +1,30 @@ -#include -#include - -#include -#include - -#include - -#include "moc_window-basic-auto-config.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "url-push-button.hpp" - -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" - -#include "ui_AutoConfigStartPage.h" -#include "ui_AutoConfigVideoPage.h" +#include "AutoConfigStreamPage.hpp" +#include "AutoConfig.hpp" #include "ui_AutoConfigStreamPage.h" -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" -#include "ui-config.h" +#include +#include +#include +#include #ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" +#include #endif +#include -struct QCef; -struct QCefCookieManager; +#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; - -#define wiz reinterpret_cast(wizard()) - -/* ------------------------------------------------------------------------- */ - -constexpr std::string_view OBSServiceFileName = "service.json"; - -static OBSData OpenServiceSettings(std::string &type) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - if (!std::filesystem::exists(jsonFilePath)) { - return OBSData(); - } - - OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - - return settings.Get(); -} - -static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) -{ - OBSData settings = OpenServiceSettings(type); - - service = obs_data_get_string(settings, "service"); - server = obs_data_get_string(settings, "server"); - key = obs_data_get_string(settings, "key"); -} - -/* ------------------------------------------------------------------------- */ - -AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) -{ - ui->setupUi(this); - setTitle(QTStr("Basic.AutoConfig.StartPage")); - setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); - - OBSBasic *main = OBSBasic::Get(); - if (main->VCamEnabled()) { - QRadioButton *prioritizeVCam = - new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); - QBoxLayout *box = reinterpret_cast(layout()); - box->insertWidget(2, prioritizeVCam); - - connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); - } -} - -AutoConfigStartPage::~AutoConfigStartPage() {} - -int AutoConfigStartPage::nextId() const -{ - return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; -} - -void AutoConfigStartPage::on_prioritizeStreaming_clicked() -{ - wiz->type = AutoConfig::Type::Streaming; -} - -void AutoConfigStartPage::on_prioritizeRecording_clicked() -{ - wiz->type = AutoConfig::Type::Recording; -} - -void AutoConfigStartPage::PrioritizeVCam() -{ - wiz->type = AutoConfig::Type::VirtualCam; -} - -/* ------------------------------------------------------------------------- */ - -#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x -#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") -#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") -#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") -#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") -#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") - -AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) -{ - ui->setupUi(this); - - setTitle(QTStr("Basic.AutoConfig.VideoPage")); - setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; - - QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); - - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); - ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); - ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); - ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); - ui->fps->setCurrentIndex(0); - - QString cxStr = QString::number(ovi.base_width); - QString cyStr = QString::number(ovi.base_height); - - int encRes = int(ovi.base_width << 16) | int(ovi.base_height); - - // Auto config only supports testing down to 240p, don't allow current - // resolution if it's lower than that. - if (ovi.base_height >= 240) - ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); - - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QSize as = screen->size(); - int as_width = as.width(); - int as_height = as.height(); - - // Calculate physical screen resolution based on the virtual screen resolution - // They might differ if scaling is enabled, e.g. for HiDPI screens - as_width = round(as_width * screen->devicePixelRatio()); - as_height = round(as_height * screen->devicePixelRatio()); - - encRes = as_width << 16 | as_height; - - QString str = - QTStr(RES_USE_DISPLAY) - .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); - - ui->canvasRes->addItem(str, encRes); - } - - auto addRes = [&](int cx, int cy) { - encRes = (cx << 16) | cy; - QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); - ui->canvasRes->addItem(str, encRes); - }; - - addRes(1920, 1080); - addRes(1280, 720); - - ui->canvasRes->setCurrentIndex(0); -} - -AutoConfigVideoPage::~AutoConfigVideoPage() {} - -int AutoConfigVideoPage::nextId() const -{ - return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; -} - -bool AutoConfigVideoPage::validatePage() -{ - int encRes = ui->canvasRes->currentData().toInt(); - wiz->baseResolutionCX = encRes >> 16; - wiz->baseResolutionCY = encRes & 0xFFFF; - wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - switch (wiz->fpsType) { - case AutoConfig::FPSType::PreferHighFPS: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = true; - break; - case AutoConfig::FPSType::PreferHighRes: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::UseCurrent: - wiz->specificFPSNum = ovi.fps_num; - wiz->specificFPSDen = ovi.fps_den; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps30: - wiz->specificFPSNum = 30; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps60: - wiz->specificFPSNum = 60; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - } - - return true; -} - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfigStreamPage.cpp" enum class ListOpt : int { ShowAll = 1, Custom, }; +struct QCef; +extern QCef *cef; + +#define wiz reinterpret_cast(wizard()) + AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) { ui->setupUi(this); @@ -910,331 +695,3 @@ void AutoConfigStreamPage::UpdateCompleted() } emit completeChanged(); } - -/* ------------------------------------------------------------------------- */ - -AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) -{ - EnableThreadedMessageBoxes(true); - - calldata_t cd = {0}; - calldata_set_int(&cd, "seconds", 5); - - proc_handler_t *ph = obs_get_proc_handler(); - proc_handler_call(ph, "twitch_ingests_refresh", &cd); - proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); - calldata_free(&cd); - - OBSBasic *main = reinterpret_cast(parent); - main->EnableOutputs(false); - - installEventFilter(CreateShortcutFilter()); - - std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); -#if defined(_WIN32) || defined(__APPLE__) - setWizardStyle(QWizard::ModernStyle); -#endif - streamPage = new AutoConfigStreamPage(); - - setPage(StartPage, new AutoConfigStartPage()); - setPage(VideoPage, new AutoConfigVideoPage()); - setPage(StreamPage, streamPage); - setPage(TestPage, new AutoConfigTestPage()); - setWindowTitle(QTStr("Basic.AutoConfig")); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - baseResolutionCX = ovi.base_width; - baseResolutionCY = ovi.base_height; - - /* ----------------------------------------- */ - /* check to see if Twitch's "auto" available */ - - OBSDataAutoRelease twitchSettings = obs_data_create(); - - obs_data_set_string(twitchSettings, "service", "Twitch"); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, twitchSettings); - - obs_property_t *p = obs_properties_get(props, "server"); - const char *first = obs_property_list_item_string(p, 0); - twitchAuto = strcmp(first, "auto") == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* check to see if Amazon IVS "auto" entries are available */ - - OBSDataAutoRelease amazonIVSSettings = obs_data_create(); - - obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); - - props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, amazonIVSSettings); - - p = obs_properties_get(props, "server"); - first = obs_property_list_item_string(p, 0); - amazonIVSAuto = strncmp(first, "auto", 4) == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* load service/servers */ - - customServer = serviceType == "rtmp_custom"; - - QComboBox *serviceList = streamPage->ui->service; - - if (!serviceName.empty()) { - serviceList->blockSignals(true); - - int count = serviceList->count(); - bool found = false; - - for (int i = 0; i < count; i++) { - QString name = serviceList->itemText(i); - - if (name == serviceName.c_str()) { - serviceList->setCurrentIndex(i); - found = true; - break; - } - } - - if (!found) { - serviceList->insertItem(0, serviceName.c_str()); - serviceList->setCurrentIndex(0); - } - - serviceList->blockSignals(false); - } - - streamPage->UpdateServerList(); - streamPage->UpdateKeyLink(); - streamPage->UpdateMoreInfoLink(); - streamPage->lastService.clear(); - - if (!customServer) { - QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); - if (idx == -1) - idx = 0; - - serverList->setCurrentIndex(idx); - } else { - streamPage->ui->customServer->setText(server.c_str()); - int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); - streamPage->ui->service->setCurrentIndex(idx); - } - - if (!key.empty()) - streamPage->ui->key->setText(key.c_str()); - - TestHardwareEncoding(); - - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - streamPage->ui->bitrate->setValue(bitrate); - streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); - streamPage->ServiceChanged(); - - if (!hardwareEncodingAvailable) { - delete streamPage->ui->preferHardware; - streamPage->ui->preferHardware = nullptr; - } else { - /* Newer generations of NVENC have a high enough quality to - * bitrate ratio that if NVENC is available, it makes sense to - * just always prefer hardware encoding by default */ - bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; - streamPage->ui->preferHardware->setChecked(preferHardware); - } - - setOptions(QWizard::WizardOptions()); - setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); - setButtonText(QWizard::BackButton, QTStr("Back")); - setButtonText(QWizard::NextButton, QTStr("Next")); - setButtonText(QWizard::CancelButton, QTStr("Cancel")); -} - -AutoConfig::~AutoConfig() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - main->EnableOutputs(true); - EnableThreadedMessageBoxes(false); -} - -void AutoConfig::TestHardwareEncoding() -{ - size_t idx = 0; - const char *id; - while (obs_enum_encoder_types(idx++, &id)) { - if (strcmp(id, "ffmpeg_nvenc") == 0) - hardwareEncodingAvailable = nvencAvailable = true; - else if (strcmp(id, "obs_qsv11") == 0) - hardwareEncodingAvailable = qsvAvailable = true; - else if (strcmp(id, "h264_texture_amf") == 0) - hardwareEncodingAvailable = vceAvailable = true; -#ifdef __APPLE__ - else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - if (__builtin_available(macOS 13.0, *)) - hardwareEncodingAvailable = appleAvailable = true; -#endif - } -} - -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (service == Service::Twitch) { - if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionUS; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAsia; - } else if (regionOther) { - return true; - } - } else { - return true; - } - - return false; -} - -void AutoConfig::done(int result) -{ - QWizard::done(result); - - if (result == QDialog::Accepted) { - if (type == Type::Streaming) - SaveStreamSettings(); - SaveSettings(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) { - OBSBasic *main = OBSBasic::Get(); - main->NewYouTubeAppDock(); - } -#endif - } -} - -inline const char *AutoConfig::GetEncoderId(Encoder enc) -{ - switch (enc) { - case Encoder::NVENC: - return SIMPLE_ENCODER_NVENC; - case Encoder::QSV: - return SIMPLE_ENCODER_QSV; - case Encoder::AMD: - return SIMPLE_ENCODER_AMD; - case Encoder::Apple: - return SIMPLE_ENCODER_APPLE_H264; - default: - return SIMPLE_ENCODER_X264; - } -}; - -void AutoConfig::SaveStreamSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - /* ---------------------------------- */ - /* save service */ - - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); - - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!streamPage->auth || !IsYouTubeService(serviceName)) - obs_data_set_string(settings, "key", key.c_str()); -#else - obs_data_set_string(settings, "key", key.c_str()); -#endif - - OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); - - if (!newService) - return; - - main->SetService(newService); - main->SaveService(); - main->auth = streamPage->auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } - - /* ---------------------------------- */ - /* save stream settings */ - - config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); - config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); - config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); - - config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); - - if (multitrackVideo.targetBitrate.has_value()) - config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", - *multitrackVideo.targetBitrate); - else - config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); - - if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && - (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - } else if (multitrackVideo.bitrate.has_value()) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); - config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", - *multitrackVideo.bitrate); - } -} - -void AutoConfig::SaveSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (recordingEncoder != Encoder::Stream) - config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); - - const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; - - config_set_string(main->Config(), "Output", "Mode", "Simple"); - config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); - config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); - config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); - config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); - config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); - - if (fpsType != FPSType::UseCurrent) { - config_set_uint(main->Config(), "Video", "FPSType", 0); - config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); - } - - main->ResetVideo(); - main->ResetOutputs(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/frontend/wizards/AutoConfigStreamPage.hpp b/frontend/wizards/AutoConfigStreamPage.hpp index 7e31bdf89..89216c461 100644 --- a/frontend/wizards/AutoConfigStreamPage.hpp +++ b/frontend/wizards/AutoConfigStreamPage.hpp @@ -1,179 +1,9 @@ #pragma once -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include - -class Ui_AutoConfigStartPage; -class Ui_AutoConfigVideoPage; -class Ui_AutoConfigStreamPage; -class Ui_AutoConfigTestPage; - -class AutoConfigStreamPage; class Auth; - -class AutoConfig : public QWizard { - Q_OBJECT - - friend class AutoConfigStartPage; - friend class AutoConfigVideoPage; - friend class AutoConfigStreamPage; - friend class AutoConfigTestPage; - - enum class Type { - Invalid, - Streaming, - Recording, - VirtualCam, - }; - - enum class Service { - Twitch, - YouTube, - AmazonIVS, - Other, - }; - - enum class Encoder { - x264, - NVENC, - QSV, - AMD, - Apple, - Stream, - }; - - enum class Quality { - Stream, - High, - }; - - enum class FPSType : int { - PreferHighFPS, - PreferHighRes, - UseCurrent, - fps30, - fps60, - }; - - struct StreamServer { - std::string name; - std::string address; - }; - - static inline const char *GetEncoderId(Encoder enc); - - AutoConfigStreamPage *streamPage = nullptr; - - Service service = Service::Other; - Quality recordingQuality = Quality::Stream; - Encoder recordingEncoder = Encoder::Stream; - Encoder streamingEncoder = Encoder::x264; - Type type = Type::Streaming; - FPSType fpsType = FPSType::PreferHighFPS; - int idealBitrate = 2500; - struct { - std::optional targetBitrate; - std::optional bitrate; - bool testSuccessful = false; - } multitrackVideo; - int baseResolutionCX = 1920; - int baseResolutionCY = 1080; - int idealResolutionCX = 1280; - int idealResolutionCY = 720; - int idealFPSNum = 60; - int idealFPSDen = 1; - std::string serviceName; - std::string serverName; - std::string server; - std::vector serviceConfigServers; - std::string key; - - bool hardwareEncodingAvailable = false; - bool nvencAvailable = false; - bool qsvAvailable = false; - bool vceAvailable = false; - bool appleAvailable = false; - - int startingBitrate = 2500; - bool customServer = false; - bool bandwidthTest = false; - bool testMultitrackVideo = false; - bool testRegions = true; - bool twitchAuto = false; - bool amazonIVSAuto = false; - bool regionUS = true; - bool regionEU = true; - bool regionAsia = true; - bool regionOther = true; - bool preferHighFPS = false; - bool preferHardware = false; - int specificFPSNum = 0; - int specificFPSDen = 0; - - void TestHardwareEncoding(); - bool CanTestServer(const char *server); - - virtual void done(int result) override; - - void SaveStreamSettings(); - void SaveSettings(); - -public: - AutoConfig(QWidget *parent); - ~AutoConfig(); - - enum Page { - StartPage, - VideoPage, - StreamPage, - TestPage, - }; -}; - -class AutoConfigStartPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigStartPage(QWidget *parent = nullptr); - ~AutoConfigStartPage(); - - virtual int nextId() const override; - -public slots: - void on_prioritizeStreaming_clicked(); - void on_prioritizeRecording_clicked(); - void PrioritizeVCam(); -}; - -class AutoConfigVideoPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigVideoPage(QWidget *parent = nullptr); - ~AutoConfigVideoPage(); - - virtual int nextId() const override; - virtual bool validatePage() override; -}; +class Ui_AutoConfigStreamPage; class AutoConfigStreamPage : public QWizardPage { Q_OBJECT @@ -219,70 +49,3 @@ public slots: void reset_service_ui_fields(std::string &service); }; - -class AutoConfigTestPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - QPointer results; - - std::unique_ptr ui; - std::thread testThread; - std::condition_variable cv; - std::mutex m; - bool cancel = false; - bool started = false; - - enum class Stage { - Starting, - BandwidthTest, - StreamEncoder, - RecordingEncoder, - Finished, - }; - - Stage stage = Stage::Starting; - bool softwareTested = false; - - void StartBandwidthStage(); - void StartStreamEncoderStage(); - void StartRecordingEncoderStage(); - - void FindIdealHardwareResolution(); - bool TestSoftwareEncoding(); - - void TestBandwidthThread(); - void TestStreamEncoderThread(); - void TestRecordingEncoderThread(); - - void FinalizeResults(); - - struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} - }; - - void GetServers(std::vector &servers); - -public: - AutoConfigTestPage(QWidget *parent = nullptr); - ~AutoConfigTestPage(); - - virtual void initializePage() override; - virtual void cleanupPage() override; - virtual bool isComplete() const override; - virtual int nextId() const override; - -public slots: - void NextStage(); - void UpdateMessage(QString message); - void Failure(QString message); - void Progress(int percentage); -}; diff --git a/frontend/wizards/AutoConfigTestPage.cpp b/frontend/wizards/AutoConfigTestPage.cpp index b53f37aa2..1d5c91850 100644 --- a/frontend/wizards/AutoConfigTestPage.cpp +++ b/frontend/wizards/AutoConfigTestPage.cpp @@ -1,86 +1,16 @@ -#include +#include "AutoConfigTestPage.hpp" +#include "AutoConfig.hpp" +#include "TestMode.hpp" +#include "ui_AutoConfigTestPage.h" -#include +#include -#include -#include -#include -#include -#include #include #include -#include "window-basic-auto-config.hpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" +#include -#include "ui_AutoConfigTestPage.h" - -#define wiz reinterpret_cast(wizard()) - -using namespace std; - -/* ------------------------------------------------------------------------- */ - -class TestMode { - obs_video_info ovi; - OBSSource source[6]; - - static void render_rand(void *, uint32_t cx, uint32_t cy) - { - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *randomvals[3] = {gs_effect_get_param_by_name(solid, "randomvals1"), - gs_effect_get_param_by_name(solid, "randomvals2"), - gs_effect_get_param_by_name(solid, "randomvals3")}; - - struct vec4 r; - - for (int i = 0; i < 3; i++) { - vec4_set(&r, rand_float(true) * 100.0f, rand_float(true) * 100.0f, - rand_float(true) * 50000.0f + 10000.0f, 0.0f); - gs_effect_set_vec4(randomvals[i], &r); - } - - while (gs_effect_loop(solid, "Random")) - gs_draw_sprite(nullptr, 0, cx, cy); - } - -public: - inline TestMode() - { - obs_get_video_info(&ovi); - obs_add_main_render_callback(render_rand, this); - - for (uint32_t i = 0; i < 6; i++) { - source[i] = obs_get_output_source(i); - obs_source_release(source[i]); - obs_set_output_source(i, nullptr); - } - } - - inline ~TestMode() - { - for (uint32_t i = 0; i < 6; i++) - obs_set_output_source(i, source[i]); - - obs_remove_main_render_callback(render_rand, this); - obs_reset_video(&ovi); - } - - inline void SetVideo(int cx, int cy, int fps_num, int fps_den) - { - obs_video_info newOVI = ovi; - - newOVI.output_width = (uint32_t)cx; - newOVI.output_height = (uint32_t)cy; - newOVI.fps_num = (uint32_t)fps_num; - newOVI.fps_den = (uint32_t)fps_den; - - obs_reset_video(&newOVI); - } -}; - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfigTestPage.cpp" #define TEST_STR(x) "Basic.AutoConfig.TestPage." x #define SUBTITLE_TESTING TEST_STR("SubTitle.Testing") @@ -97,6 +27,10 @@ public: #define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder") #define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder") +#define wiz reinterpret_cast(wizard()) + +using namespace std; + void AutoConfigTestPage::StartBandwidthStage() { ui->progressLabel->setText(QTStr(TEST_BW)); @@ -152,14 +86,7 @@ static inline void string_depad_key(string &key) } } -const char *FindAudioEncoderFromCodec(const char *type); - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} +extern const char *FindAudioEncoderFromCodec(const char *type); static bool return_first_id(void *data, const char *id) { diff --git a/frontend/wizards/AutoConfigVideoPage.cpp b/frontend/wizards/AutoConfigVideoPage.cpp index 6b1667609..69de8e2ec 100644 --- a/frontend/wizards/AutoConfigVideoPage.cpp +++ b/frontend/wizards/AutoConfigVideoPage.cpp @@ -1,118 +1,12 @@ -#include +#include "AutoConfigVideoPage.hpp" +#include "AutoConfig.hpp" +#include "ui_AutoConfigVideoPage.h" + +#include + #include -#include -#include - -#include - -#include "moc_window-basic-auto-config.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "url-push-button.hpp" - -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" - -#include "ui_AutoConfigStartPage.h" -#include "ui_AutoConfigVideoPage.h" -#include "ui_AutoConfigStreamPage.h" - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" -#include "ui-config.h" -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; - -#define wiz reinterpret_cast(wizard()) - -/* ------------------------------------------------------------------------- */ - -constexpr std::string_view OBSServiceFileName = "service.json"; - -static OBSData OpenServiceSettings(std::string &type) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - if (!std::filesystem::exists(jsonFilePath)) { - return OBSData(); - } - - OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - - return settings.Get(); -} - -static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) -{ - OBSData settings = OpenServiceSettings(type); - - service = obs_data_get_string(settings, "service"); - server = obs_data_get_string(settings, "server"); - key = obs_data_get_string(settings, "key"); -} - -/* ------------------------------------------------------------------------- */ - -AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) -{ - ui->setupUi(this); - setTitle(QTStr("Basic.AutoConfig.StartPage")); - setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); - - OBSBasic *main = OBSBasic::Get(); - if (main->VCamEnabled()) { - QRadioButton *prioritizeVCam = - new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); - QBoxLayout *box = reinterpret_cast(layout()); - box->insertWidget(2, prioritizeVCam); - - connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); - } -} - -AutoConfigStartPage::~AutoConfigStartPage() {} - -int AutoConfigStartPage::nextId() const -{ - return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; -} - -void AutoConfigStartPage::on_prioritizeStreaming_clicked() -{ - wiz->type = AutoConfig::Type::Streaming; -} - -void AutoConfigStartPage::on_prioritizeRecording_clicked() -{ - wiz->type = AutoConfig::Type::Recording; -} - -void AutoConfigStartPage::PrioritizeVCam() -{ - wiz->type = AutoConfig::Type::VirtualCam; -} - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfigVideoPage.cpp" #define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x #define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") @@ -121,6 +15,8 @@ void AutoConfigStartPage::PrioritizeVCam() #define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") #define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") +#define wiz reinterpret_cast(wizard()) + AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) { ui->setupUi(this); @@ -232,1009 +128,3 @@ bool AutoConfigVideoPage::validatePage() return true; } - -/* ------------------------------------------------------------------------- */ - -enum class ListOpt : int { - ShowAll = 1, - Custom, -}; - -AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) -{ - ui->setupUi(this); - ui->bitrateLabel->setVisible(false); - ui->bitrate->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - ui->useMultitrackVideo->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - int vertSpacing = ui->topLayout->verticalSpacing(); - - QMargins m = ui->topLayout->contentsMargins(); - m.setBottom(vertSpacing / 2); - ui->topLayout->setContentsMargins(m); - - m = ui->loginPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->loginPageLayout->setContentsMargins(m); - - m = ui->streamkeyPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); - - setTitle(QTStr("Basic.AutoConfig.StreamPage")); - setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); - - LoadServices(false); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged); - connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList); - - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink); - connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink); - - connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() { - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - }); - - connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); - connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted); -} - -AutoConfigStreamPage::~AutoConfigStreamPage() {} - -bool AutoConfigStreamPage::isComplete() const -{ - return ready; -} - -int AutoConfigStreamPage::nextId() const -{ - return AutoConfig::TestPage; -} - -inline bool AutoConfigStreamPage::IsCustomService() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::Custom; -} - -bool AutoConfigStreamPage::validatePage() -{ - OBSDataAutoRelease service_settings = obs_data_create(); - - wiz->customServer = IsCustomService(); - - const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; - - if (!wiz->customServer) { - obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText())); - } - - OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr); - - int bitrate; - if (!ui->doBandwidthTest->isChecked()) { - bitrate = ui->bitrate->value(); - wiz->idealBitrate = bitrate; - } else { - /* Default test target is 10 Mbps */ - bitrate = 10000; -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(wiz->serviceName)) { - /* Adjust upper bound to YouTube limits - * for resolutions above 1080p */ - if (wiz->baseResolutionCY > 1440) - bitrate = 51000; - else if (wiz->baseResolutionCY > 1080) - bitrate = 18000; - } -#endif - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(service, settings, nullptr); - - if (wiz->customServer) { - QString server = ui->customServer->text().trimmed(); - wiz->server = wiz->serverName = QT_TO_UTF8(server); - } else { - wiz->serverName = QT_TO_UTF8(ui->server->currentText()); - wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); - } - - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); - wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); - wiz->idealBitrate = wiz->startingBitrate; - wiz->regionUS = ui->regionUS->isChecked(); - wiz->regionEU = ui->regionEU->isChecked(); - wiz->regionAsia = ui->regionAsia->isChecked(); - wiz->regionOther = ui->regionOther->isChecked(); - wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); - if (ui->preferHardware) - wiz->preferHardware = ui->preferHardware->isChecked(); - wiz->key = QT_TO_UTF8(ui->key->text()); - - if (!wiz->customServer) { - if (wiz->serviceName == "Twitch") - wiz->service = AutoConfig::Service::Twitch; -#ifdef YOUTUBE_ENABLED - else if (IsYouTubeService(wiz->serviceName)) - wiz->service = AutoConfig::Service::YouTube; -#endif - else if (wiz->serviceName == "Amazon IVS") - wiz->service = AutoConfig::Service::AmazonIVS; - else - wiz->service = AutoConfig::Service::Other; - } else { - wiz->service = AutoConfig::Service::Other; - } - - if (wiz->service == AutoConfig::Service::Twitch) { - wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); - - if (wiz->testMultitrackVideo) { - auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt, - std::nullopt, false); - - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - try { - auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service), - postData, multitrack_video_name); - - for (const auto &endpoint : config.ingest_endpoints) { - if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0) - continue; - - std::string address = endpoint.url_template; - auto pos = address.find("/{stream_key}"); - if (pos != address.npos) - address.erase(pos); - - wiz->serviceConfigServers.push_back({address, address}); - } - - int multitrackVideoBitrate = 0; - for (auto &encoder_config : config.encoder_configurations) { - auto it = encoder_config.settings.find("bitrate"); - if (it == encoder_config.settings.end()) - continue; - - if (!it->is_number_integer()) - continue; - - int bitrate = 0; - it->get_to(bitrate); - multitrackVideoBitrate += bitrate; - } - - if (multitrackVideoBitrate > 0) { - wiz->startingBitrate = multitrackVideoBitrate; - wiz->idealBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate; - wiz->multitrackVideo.testSuccessful = true; - } - } catch (const MultitrackVideoError & /*err*/) { - // FIXME: do something sensible - } - } - } - - if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && - wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) { - QMessageBox::StandardButton button; -#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) - button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text")); -#undef WARNING_TEXT - - if (button == QMessageBox::No) - return false; - } - - return true; -} - -void AutoConfigStreamPage::on_show_clicked() -{ - if (ui->key->echoMode() == QLineEdit::Password) { - ui->key->setEchoMode(QLineEdit::Normal); - ui->show->setText(QTStr("Hide")); - } else { - ui->key->setEchoMode(QLineEdit::Password); - ui->show->setText(QTStr("Show")); - } -} - -void AutoConfigStreamPage::OnOAuthStreamKeyConnected() -{ - OAuthStreamKey *a = reinterpret_cast(auth.get()); - - if (a) { - bool validKey = !a->key().empty(); - - if (validKey) - ui->key->setText(QT_UTF8(a->key().c_str())); - - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(a->service())) { - ui->key->clear(); - - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - - ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title")); - - YoutubeApiWrappers *ytAuth = reinterpret_cast(a); - ChannelDescription cd; - if (ytAuth->GetChannelDescription(cd)) { - ui->connectedAccountText->setText(cd.title); - - /* Create throwaway stream key for bandwidth test */ - if (ui->doBandwidthTest->isChecked()) { - StreamDescription stream = {"", "", "OBS Studio Test Stream"}; - if (ytAuth->InsertStream(stream)) { - ui->key->setText(stream.name); - } - } - } - } -#endif - } - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::OnAuthConnected() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - Auth::Type type = Auth::AuthType(service); - - if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) { - OnOAuthStreamKeyConnected(); - } -} - -void AutoConfigStreamPage::on_connectAccount_clicked() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - - OAuth::DeleteCookies(service); - - auth = OAuthStreamKey::Login(this, service); - if (!!auth) { - OnAuthConnected(); - - ui->useStreamKeyAdv->setVisible(false); - } -} - -#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" -#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" - -void AutoConfigStreamPage::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { - return; - } - - OBSBasic *main = OBSBasic::Get(); - - main->auth.reset(); - auth.reset(); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif - - reset_service_ui_fields(service); - - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - /* Restore key link when disconnecting account */ - UpdateKeyLink(); -} - -void AutoConfigStreamPage::on_useStreamKey_clicked() -{ - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::on_preferHardware_clicked() -{ - auto *main = OBSBasic::Get(); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - - ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); - ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); - ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled); -} - -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - -static inline bool is_external_oauth(const std::string &service) -{ - return Auth::External(service); -} - -void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) -{ -#ifdef YOUTUBE_ENABLED - // when account is already connected: - OAuthStreamKey *a = reinterpret_cast(auth.get()); - if (a && service == a->service() && IsYouTubeService(a->service())) { - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - return; - } -#endif - - bool external_oauth = is_external_oauth(service); - if (external_oauth) { - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(true); - ui->useStreamKeyAdv->setVisible(true); - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey; - - ui->stackedWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - ui->useStreamKeyAdv->setVisible(false); - } else { - ui->connectAccount2->setVisible(false); - ui->useStreamKeyAdv->setVisible(false); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} - -void AutoConfigStreamPage::ServiceChanged() -{ - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - if (showMore) - return; - - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool regionBased = service == "Twitch"; - bool testBandwidth = ui->doBandwidthTest->isChecked(); - bool custom = IsCustomService(); - - bool ertmp_multitrack_video_available = service == "Twitch"; - - bool custom_disclaimer = false; - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (!custom) { - OBSDataAutoRelease service_settings = obs_data_create(); - obs_data_set_string(service_settings, "service", service.c_str()); - OBSServiceAutoRelease obs_service = - obs_service_create("rtmp_common", "temp service", service_settings, nullptr); - - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) { - ui->multitrackVideoInfo->setText( - obs_data_get_string(service_settings, "multitrack_video_disclaimer")); - custom_disclaimer = true; - } - } - - if (!custom_disclaimer) { - ui->multitrackVideoInfo->setText( - QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str())); - } - - ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); - ui->useMultitrackVideo->setText( - QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name)); - ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); - ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); - - reset_service_ui_fields(service); - - /* Test three closest servers if "Auto" is available for Twitch */ - if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto)) - regionBased = false; - - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); - ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); - - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(false); - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - } else { - if (!testBandwidth) - ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(regionBased && testBandwidth); - ui->serverStackedWidget->setCurrentIndex(0); - ui->serverStackedWidget->setHidden(testBandwidth); - ui->serverLabel->setHidden(testBandwidth); - } - - wiz->testRegions = regionBased && testBandwidth; - - ui->bitrateLabel->setHidden(testBandwidth); - ui->bitrate->setHidden(testBandwidth); - - OBSBasic *main = OBSBasic::Get(); - - if (main->auth) { - auto system_auth_service = main->auth->service(); - bool service_check = service.find(system_auth_service) != std::string::npos; -#ifdef YOUTUBE_ENABLED - service_check = service_check ? service_check - : IsYouTubeService(system_auth_service) && IsYouTubeService(service); -#endif - if (service_check) { - auth.reset(); - auth = main->auth; - OnAuthConnected(); - } - } - - UpdateCompleted(); -} - -void AutoConfigStreamPage::UpdateMoreInfoLink() -{ - if (IsCustomService()) { - ui->moreInfoButton->hide(); - return; - } - - QString serviceName = ui->service->currentText(); - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - const char *more_info_link = obs_data_get_string(settings, "more_info_link"); - - if (!more_info_link || (*more_info_link == '\0')) { - ui->moreInfoButton->hide(); - } else { - ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); - ui->moreInfoButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateKeyLink() -{ - QString serviceName = ui->service->currentText(); - QString customServer = ui->customServer->text().trimmed(); - QString streamKeyLink; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); - - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; - } - - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - ui->streamKeyLabel->setToolTip(""); - } else if (!IsCustomService()) { - ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - ui->streamKeyLabel->setToolTip(""); - } else { - /* add tooltips for stream key */ - QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg"; - QString lStr = "%1 "; - - ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file)); - ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); - } - - if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) { - ui->streamKeyButton->hide(); - } else { - ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->streamKeyButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_bool(settings, "show_all", showAll); - - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); - - ui->service->blockSignals(true); - ui->service->clear(); - - QStringList names; - - obs_property_t *services = obs_properties_get(props, "service"); - size_t services_count = obs_property_list_item_count(services); - for (size_t i = 0; i < services_count; i++) { - const char *name = obs_property_list_item_string(services, i); - names.push_back(name); - } - - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { - ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); - } - - ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); - - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); - } - - obs_properties_destroy(props); - - ui->service->blockSignals(false); -} - -void AutoConfigStreamPage::UpdateServerList() -{ - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; - - if (showMore) { - LoadServices(true); - ui->service->showPopup(); - return; - } else { - lastService = serviceName; - } - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - obs_property_t *servers = obs_properties_get(props, "server"); - - ui->server->clear(); - - size_t servers_count = obs_property_list_item_count(servers); - for (size_t i = 0; i < servers_count; i++) { - const char *name = obs_property_list_item_name(servers, i); - const char *server = obs_property_list_item_string(servers, i); - ui->server->addItem(name, server); - } - - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateCompleted() -{ - const bool custom = IsCustomService(); - if (ui->stackedWidget->currentIndex() == (int)Section::Connect || - (ui->key->text().isEmpty() && !auth && !custom)) { - ready = false; - } else { - if (custom) { - ready = !ui->customServer->text().isEmpty(); - } else { - ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() || - ui->regionAsia->isChecked() || ui->regionOther->isChecked(); - } - } - emit completeChanged(); -} - -/* ------------------------------------------------------------------------- */ - -AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) -{ - EnableThreadedMessageBoxes(true); - - calldata_t cd = {0}; - calldata_set_int(&cd, "seconds", 5); - - proc_handler_t *ph = obs_get_proc_handler(); - proc_handler_call(ph, "twitch_ingests_refresh", &cd); - proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); - calldata_free(&cd); - - OBSBasic *main = reinterpret_cast(parent); - main->EnableOutputs(false); - - installEventFilter(CreateShortcutFilter()); - - std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); -#if defined(_WIN32) || defined(__APPLE__) - setWizardStyle(QWizard::ModernStyle); -#endif - streamPage = new AutoConfigStreamPage(); - - setPage(StartPage, new AutoConfigStartPage()); - setPage(VideoPage, new AutoConfigVideoPage()); - setPage(StreamPage, streamPage); - setPage(TestPage, new AutoConfigTestPage()); - setWindowTitle(QTStr("Basic.AutoConfig")); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - baseResolutionCX = ovi.base_width; - baseResolutionCY = ovi.base_height; - - /* ----------------------------------------- */ - /* check to see if Twitch's "auto" available */ - - OBSDataAutoRelease twitchSettings = obs_data_create(); - - obs_data_set_string(twitchSettings, "service", "Twitch"); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, twitchSettings); - - obs_property_t *p = obs_properties_get(props, "server"); - const char *first = obs_property_list_item_string(p, 0); - twitchAuto = strcmp(first, "auto") == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* check to see if Amazon IVS "auto" entries are available */ - - OBSDataAutoRelease amazonIVSSettings = obs_data_create(); - - obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); - - props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, amazonIVSSettings); - - p = obs_properties_get(props, "server"); - first = obs_property_list_item_string(p, 0); - amazonIVSAuto = strncmp(first, "auto", 4) == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* load service/servers */ - - customServer = serviceType == "rtmp_custom"; - - QComboBox *serviceList = streamPage->ui->service; - - if (!serviceName.empty()) { - serviceList->blockSignals(true); - - int count = serviceList->count(); - bool found = false; - - for (int i = 0; i < count; i++) { - QString name = serviceList->itemText(i); - - if (name == serviceName.c_str()) { - serviceList->setCurrentIndex(i); - found = true; - break; - } - } - - if (!found) { - serviceList->insertItem(0, serviceName.c_str()); - serviceList->setCurrentIndex(0); - } - - serviceList->blockSignals(false); - } - - streamPage->UpdateServerList(); - streamPage->UpdateKeyLink(); - streamPage->UpdateMoreInfoLink(); - streamPage->lastService.clear(); - - if (!customServer) { - QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); - if (idx == -1) - idx = 0; - - serverList->setCurrentIndex(idx); - } else { - streamPage->ui->customServer->setText(server.c_str()); - int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); - streamPage->ui->service->setCurrentIndex(idx); - } - - if (!key.empty()) - streamPage->ui->key->setText(key.c_str()); - - TestHardwareEncoding(); - - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - streamPage->ui->bitrate->setValue(bitrate); - streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); - streamPage->ServiceChanged(); - - if (!hardwareEncodingAvailable) { - delete streamPage->ui->preferHardware; - streamPage->ui->preferHardware = nullptr; - } else { - /* Newer generations of NVENC have a high enough quality to - * bitrate ratio that if NVENC is available, it makes sense to - * just always prefer hardware encoding by default */ - bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; - streamPage->ui->preferHardware->setChecked(preferHardware); - } - - setOptions(QWizard::WizardOptions()); - setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); - setButtonText(QWizard::BackButton, QTStr("Back")); - setButtonText(QWizard::NextButton, QTStr("Next")); - setButtonText(QWizard::CancelButton, QTStr("Cancel")); -} - -AutoConfig::~AutoConfig() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - main->EnableOutputs(true); - EnableThreadedMessageBoxes(false); -} - -void AutoConfig::TestHardwareEncoding() -{ - size_t idx = 0; - const char *id; - while (obs_enum_encoder_types(idx++, &id)) { - if (strcmp(id, "ffmpeg_nvenc") == 0) - hardwareEncodingAvailable = nvencAvailable = true; - else if (strcmp(id, "obs_qsv11") == 0) - hardwareEncodingAvailable = qsvAvailable = true; - else if (strcmp(id, "h264_texture_amf") == 0) - hardwareEncodingAvailable = vceAvailable = true; -#ifdef __APPLE__ - else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - if (__builtin_available(macOS 13.0, *)) - hardwareEncodingAvailable = appleAvailable = true; -#endif - } -} - -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (service == Service::Twitch) { - if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionUS; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAsia; - } else if (regionOther) { - return true; - } - } else { - return true; - } - - return false; -} - -void AutoConfig::done(int result) -{ - QWizard::done(result); - - if (result == QDialog::Accepted) { - if (type == Type::Streaming) - SaveStreamSettings(); - SaveSettings(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) { - OBSBasic *main = OBSBasic::Get(); - main->NewYouTubeAppDock(); - } -#endif - } -} - -inline const char *AutoConfig::GetEncoderId(Encoder enc) -{ - switch (enc) { - case Encoder::NVENC: - return SIMPLE_ENCODER_NVENC; - case Encoder::QSV: - return SIMPLE_ENCODER_QSV; - case Encoder::AMD: - return SIMPLE_ENCODER_AMD; - case Encoder::Apple: - return SIMPLE_ENCODER_APPLE_H264; - default: - return SIMPLE_ENCODER_X264; - } -}; - -void AutoConfig::SaveStreamSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - /* ---------------------------------- */ - /* save service */ - - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); - - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!streamPage->auth || !IsYouTubeService(serviceName)) - obs_data_set_string(settings, "key", key.c_str()); -#else - obs_data_set_string(settings, "key", key.c_str()); -#endif - - OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); - - if (!newService) - return; - - main->SetService(newService); - main->SaveService(); - main->auth = streamPage->auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } - - /* ---------------------------------- */ - /* save stream settings */ - - config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); - config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); - config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); - - config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); - - if (multitrackVideo.targetBitrate.has_value()) - config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", - *multitrackVideo.targetBitrate); - else - config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); - - if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && - (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - } else if (multitrackVideo.bitrate.has_value()) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); - config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", - *multitrackVideo.bitrate); - } -} - -void AutoConfig::SaveSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (recordingEncoder != Encoder::Stream) - config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); - - const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; - - config_set_string(main->Config(), "Output", "Mode", "Simple"); - config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); - config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); - config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); - config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); - config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); - - if (fpsType != FPSType::UseCurrent) { - config_set_uint(main->Config(), "Video", "FPSType", 0); - config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); - } - - main->ResetVideo(); - main->ResetOutputs(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/frontend/wizards/AutoConfigVideoPage.hpp b/frontend/wizards/AutoConfigVideoPage.hpp index 7e31bdf89..b6a441d2c 100644 --- a/frontend/wizards/AutoConfigVideoPage.hpp +++ b/frontend/wizards/AutoConfigVideoPage.hpp @@ -1,164 +1,8 @@ #pragma once -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include - -class Ui_AutoConfigStartPage; class Ui_AutoConfigVideoPage; -class Ui_AutoConfigStreamPage; -class Ui_AutoConfigTestPage; - -class AutoConfigStreamPage; -class Auth; - -class AutoConfig : public QWizard { - Q_OBJECT - - friend class AutoConfigStartPage; - friend class AutoConfigVideoPage; - friend class AutoConfigStreamPage; - friend class AutoConfigTestPage; - - enum class Type { - Invalid, - Streaming, - Recording, - VirtualCam, - }; - - enum class Service { - Twitch, - YouTube, - AmazonIVS, - Other, - }; - - enum class Encoder { - x264, - NVENC, - QSV, - AMD, - Apple, - Stream, - }; - - enum class Quality { - Stream, - High, - }; - - enum class FPSType : int { - PreferHighFPS, - PreferHighRes, - UseCurrent, - fps30, - fps60, - }; - - struct StreamServer { - std::string name; - std::string address; - }; - - static inline const char *GetEncoderId(Encoder enc); - - AutoConfigStreamPage *streamPage = nullptr; - - Service service = Service::Other; - Quality recordingQuality = Quality::Stream; - Encoder recordingEncoder = Encoder::Stream; - Encoder streamingEncoder = Encoder::x264; - Type type = Type::Streaming; - FPSType fpsType = FPSType::PreferHighFPS; - int idealBitrate = 2500; - struct { - std::optional targetBitrate; - std::optional bitrate; - bool testSuccessful = false; - } multitrackVideo; - int baseResolutionCX = 1920; - int baseResolutionCY = 1080; - int idealResolutionCX = 1280; - int idealResolutionCY = 720; - int idealFPSNum = 60; - int idealFPSDen = 1; - std::string serviceName; - std::string serverName; - std::string server; - std::vector serviceConfigServers; - std::string key; - - bool hardwareEncodingAvailable = false; - bool nvencAvailable = false; - bool qsvAvailable = false; - bool vceAvailable = false; - bool appleAvailable = false; - - int startingBitrate = 2500; - bool customServer = false; - bool bandwidthTest = false; - bool testMultitrackVideo = false; - bool testRegions = true; - bool twitchAuto = false; - bool amazonIVSAuto = false; - bool regionUS = true; - bool regionEU = true; - bool regionAsia = true; - bool regionOther = true; - bool preferHighFPS = false; - bool preferHardware = false; - int specificFPSNum = 0; - int specificFPSDen = 0; - - void TestHardwareEncoding(); - bool CanTestServer(const char *server); - - virtual void done(int result) override; - - void SaveStreamSettings(); - void SaveSettings(); - -public: - AutoConfig(QWidget *parent); - ~AutoConfig(); - - enum Page { - StartPage, - VideoPage, - StreamPage, - TestPage, - }; -}; - -class AutoConfigStartPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigStartPage(QWidget *parent = nullptr); - ~AutoConfigStartPage(); - - virtual int nextId() const override; - -public slots: - void on_prioritizeStreaming_clicked(); - void on_prioritizeRecording_clicked(); - void PrioritizeVCam(); -}; class AutoConfigVideoPage : public QWizardPage { Q_OBJECT @@ -174,115 +18,3 @@ public: virtual int nextId() const override; virtual bool validatePage() override; }; - -class AutoConfigStreamPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - - std::unique_ptr ui; - QString lastService; - bool ready = false; - - void LoadServices(bool showAll); - inline bool IsCustomService() const; - -public: - AutoConfigStreamPage(QWidget *parent = nullptr); - ~AutoConfigStreamPage(); - - virtual bool isComplete() const override; - virtual int nextId() const override; - virtual bool validatePage() override; - - void OnAuthConnected(); - void OnOAuthStreamKeyConnected(); - -public slots: - void on_show_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_preferHardware_clicked(); - void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); - void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); -}; - -class AutoConfigTestPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - QPointer results; - - std::unique_ptr ui; - std::thread testThread; - std::condition_variable cv; - std::mutex m; - bool cancel = false; - bool started = false; - - enum class Stage { - Starting, - BandwidthTest, - StreamEncoder, - RecordingEncoder, - Finished, - }; - - Stage stage = Stage::Starting; - bool softwareTested = false; - - void StartBandwidthStage(); - void StartStreamEncoderStage(); - void StartRecordingEncoderStage(); - - void FindIdealHardwareResolution(); - bool TestSoftwareEncoding(); - - void TestBandwidthThread(); - void TestStreamEncoderThread(); - void TestRecordingEncoderThread(); - - void FinalizeResults(); - - struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} - }; - - void GetServers(std::vector &servers); - -public: - AutoConfigTestPage(QWidget *parent = nullptr); - ~AutoConfigTestPage(); - - virtual void initializePage() override; - virtual void cleanupPage() override; - virtual bool isComplete() const override; - virtual int nextId() const override; - -public slots: - void NextStage(); - void UpdateMessage(QString message); - void Failure(QString message); - void Progress(int percentage); -}; diff --git a/frontend/wizards/TestMode.hpp b/frontend/wizards/TestMode.hpp index b53f37aa2..fd9b80646 100644 --- a/frontend/wizards/TestMode.hpp +++ b/frontend/wizards/TestMode.hpp @@ -1,26 +1,7 @@ -#include +#pragma once -#include - -#include -#include -#include -#include -#include #include -#include - -#include "window-basic-auto-config.hpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" - -#include "ui_AutoConfigTestPage.h" - -#define wiz reinterpret_cast(wizard()) - -using namespace std; - -/* ------------------------------------------------------------------------- */ +#include class TestMode { obs_video_info ovi; @@ -79,1183 +60,3 @@ public: obs_reset_video(&newOVI); } }; - -/* ------------------------------------------------------------------------- */ - -#define TEST_STR(x) "Basic.AutoConfig.TestPage." x -#define SUBTITLE_TESTING TEST_STR("SubTitle.Testing") -#define SUBTITLE_COMPLETE TEST_STR("SubTitle.Complete") -#define TEST_BW TEST_STR("TestingBandwidth") -#define TEST_BW_NO_OUTPUT TEST_STR("TestingBandwidth.NoOutput") -#define TEST_BW_CONNECTING TEST_STR("TestingBandwidth.Connecting") -#define TEST_BW_CONNECT_FAIL TEST_STR("TestingBandwidth.ConnectFailed") -#define TEST_BW_SERVER TEST_STR("TestingBandwidth.Server") -#define TEST_RES_VAL TEST_STR("TestingRes.Resolution") -#define TEST_RES_FAIL TEST_STR("TestingRes.Fail") -#define TEST_SE TEST_STR("TestingStreamEncoder") -#define TEST_RE TEST_STR("TestingRecordingEncoder") -#define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder") -#define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder") - -void AutoConfigTestPage::StartBandwidthStage() -{ - ui->progressLabel->setText(QTStr(TEST_BW)); - testThread = std::thread([this]() { TestBandwidthThread(); }); -} - -void AutoConfigTestPage::StartStreamEncoderStage() -{ - ui->progressLabel->setText(QTStr(TEST_SE)); - testThread = std::thread([this]() { TestStreamEncoderThread(); }); -} - -void AutoConfigTestPage::StartRecordingEncoderStage() -{ - ui->progressLabel->setText(QTStr(TEST_RE)); - testThread = std::thread([this]() { TestRecordingEncoderThread(); }); -} - -void AutoConfigTestPage::GetServers(std::vector &servers) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "service", wiz->serviceName.c_str()); - - obs_properties_t *ppts = obs_get_service_properties("rtmp_common"); - obs_property_t *p = obs_properties_get(ppts, "service"); - obs_property_modified(p, settings); - - p = obs_properties_get(ppts, "server"); - size_t count = obs_property_list_item_count(p); - servers.reserve(count); - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *server = obs_property_list_item_string(p, i); - - if (wiz->CanTestServer(name)) { - ServerInfo info(name, server); - servers.push_back(info); - } - } - - obs_properties_destroy(ppts); -} - -static inline void string_depad_key(string &key) -{ - while (!key.empty()) { - char ch = key.back(); - if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') - key.pop_back(); - else - break; - } -} - -const char *FindAudioEncoderFromCodec(const char *type); - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -void AutoConfigTestPage::TestBandwidthThread() -{ - bool connected = false; - bool stopped = false; - - TestMode testMode; - testMode.SetVideo(128, 128, 60, 1); - - QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, 0)); - - /* - * create encoders - * create output - * test for 10 seconds - */ - - QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral(""))); - - /* -----------------------------------*/ - /* create obs objects */ - - const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; - - OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr); - OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSServiceAutoRelease service = obs_service_create(serverType, "test_service", nullptr, nullptr); - - /* -----------------------------------*/ - /* configure settings */ - - // service: "service", "server", "key" - // vencoder: "bitrate", "rate_control", - // obs_service_apply_encoder_settings - // aencoder: "bitrate" - // output: "bind_ip" via main config -> "Output", "BindIP" - // obs_output_set_service - - OBSDataAutoRelease service_settings = obs_data_create(); - OBSDataAutoRelease vencoder_settings = obs_data_create(); - OBSDataAutoRelease aencoder_settings = obs_data_create(); - OBSDataAutoRelease output_settings = obs_data_create(); - - std::string key = wiz->key; - if (wiz->service == AutoConfig::Service::Twitch || wiz->service == AutoConfig::Service::AmazonIVS) { - string_depad_key(key); - key += "?bandwidthtest"; - } else if (wiz->serviceName == "Restream.io" || wiz->serviceName == "Restream.io - RTMP") { - string_depad_key(key); - key += "?test=true"; - } - - obs_data_set_string(service_settings, "service", wiz->serviceName.c_str()); - obs_data_set_string(service_settings, "key", key.c_str()); - - obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); - obs_data_set_string(vencoder_settings, "rate_control", "CBR"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - obs_data_set_int(vencoder_settings, "keyint_sec", 2); - - obs_data_set_int(aencoder_settings, "bitrate", 32); - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *bind_ip = config_get_string(main->Config(), "Output", "BindIP"); - obs_data_set_string(output_settings, "bind_ip", bind_ip); - - const char *ip_family = config_get_string(main->Config(), "Output", "IPFamily"); - obs_data_set_string(output_settings, "ip_family", ip_family); - - /* -----------------------------------*/ - /* determine which servers to test */ - - std::vector servers; - if (wiz->customServer) - servers.emplace_back(wiz->server.c_str(), wiz->server.c_str()); - else - GetServers(servers); - - /* just use the first server if it only has one alternate server, - * or if using Restream or Nimo TV due to their "auto" servers */ - if (servers.size() < 3 || wiz->serviceName.substr(0, 11) == "Restream.io" || wiz->serviceName == "Nimo TV") { - servers.resize(1); - - } else if ((wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) || - (wiz->service == AutoConfig::Service::AmazonIVS && wiz->amazonIVSAuto)) { - /* if using Twitch and "Auto" is available, test 3 closest - * server */ - servers.erase(servers.begin() + 1); - servers.resize(3); - } else if (wiz->service == AutoConfig::Service::YouTube) { - /* Only test first set of primary + backup servers */ - servers.resize(2); - } - - if (!wiz->serviceConfigServers.empty()) { - if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) { - // servers from Twitch service config replace the "auto" entry - servers.erase(servers.begin()); - } - - for (auto it = std::rbegin(wiz->serviceConfigServers); it != std::rend(wiz->serviceConfigServers); - it++) { - auto same_server = - std::find_if(std::begin(servers), std::end(servers), - [&](const ServerInfo &si) { return si.address == it->address; }); - if (same_server != std::end(servers)) - servers.erase(same_server); - servers.emplace(std::begin(servers), it->name.c_str(), it->address.c_str()); - } - - if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) { - // see above, only test 3 servers - // rtmps urls are currently counted as separate servers - servers.resize(3); - } - } - - /* -----------------------------------*/ - /* apply service settings */ - - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings); - - if (wiz->multitrackVideo.testSuccessful) { - obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); - } - - /* -----------------------------------*/ - /* create output */ - - /* Check if the service has a preferred output type */ - const char *output_type = obs_service_get_preferred_output_type(service); - if (!output_type || (obs_get_output_flags(output_type) & OBS_OUTPUT_SERVICE) == 0) { - /* Otherwise, prefer first-party output types */ - const char *protocol = obs_service_get_protocol(service); - - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - output_type = "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - output_type = "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - output_type = "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - if (!output_type) - obs_enum_output_types_with_protocol(protocol, &output_type, return_first_id); - - /* If none, fail */ - if (!output_type) { - QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_NO_OUTPUT))); - return; - } - } - - OBSOutputAutoRelease output = obs_output_create(output_type, "test_stream", nullptr, nullptr); - obs_output_update(output, output_settings); - - const char *audio_codec = obs_output_get_supported_audio_codecs(output); - - if (strcmp(audio_codec, "aac") != 0) { - const char *id = FindAudioEncoderFromCodec(audio_codec); - aencoder = obs_audio_encoder_create(id, "test_audio", nullptr, 0, nullptr); - } - - /* -----------------------------------*/ - /* connect encoders/services/outputs */ - - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); - obs_encoder_set_video(vencoder, obs_get_video()); - obs_encoder_set_audio(aencoder, obs_get_audio()); - - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); - obs_output_set_reconnect_settings(output, 0, 0); - - obs_output_set_service(output, service); - - /* -----------------------------------*/ - /* connect signals */ - - auto on_started = [&]() { - unique_lock lock(m); - connected = true; - stopped = false; - cv.notify_one(); - }; - - auto on_stopped = [&]() { - unique_lock lock(m); - connected = false; - stopped = true; - cv.notify_one(); - }; - - using on_started_t = decltype(on_started); - using on_stopped_t = decltype(on_stopped); - - auto pre_on_started = [](void *data, calldata_t *) { - on_started_t &on_started = *reinterpret_cast(data); - on_started(); - }; - - auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = *reinterpret_cast(data); - on_stopped(); - }; - - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "start", pre_on_started, &on_started); - signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); - - /* -----------------------------------*/ - /* test servers */ - - bool success = false; - - for (size_t i = 0; i < servers.size(); i++) { - auto &server = servers[i]; - - connected = false; - stopped = false; - - int per = int((i + 1) * 100 / servers.size()); - QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); - QMetaObject::invokeMethod(this, "UpdateMessage", - Q_ARG(QString, QTStr(TEST_BW_CONNECTING).arg(server.name.c_str()))); - - obs_data_set_string(service_settings, "server", server.address.c_str()); - obs_service_update(service, service_settings); - - if (!obs_output_start(output)) - continue; - - unique_lock ul(m); - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return; - } - if (!stopped && !connected) - cv.wait(ul); - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return; - } - if (!connected) - continue; - - QMetaObject::invokeMethod(this, "UpdateMessage", - Q_ARG(QString, QTStr(TEST_BW_SERVER).arg(server.name.c_str()))); - - /* ignore first 2.5 seconds due to possible buffering skewing - * the result */ - cv.wait_for(ul, chrono::milliseconds(2500)); - if (stopped) - continue; - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return; - } - - /* continue test */ - int start_bytes = (int)obs_output_get_total_bytes(output); - uint64_t t_start = os_gettime_ns(); - - cv.wait_for(ul, chrono::seconds(10)); - if (stopped) - continue; - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return; - } - - obs_output_stop(output); - cv.wait(ul); - - uint64_t total_time = os_gettime_ns() - t_start; - if (total_time == 0) - total_time = 1; - - int total_bytes = (int)obs_output_get_total_bytes(output) - start_bytes; - uint64_t bitrate = util_mul_div64(total_bytes, 8ULL * 1000000000ULL / 1000ULL, total_time); - if (obs_output_get_frames_dropped(output) || (int)bitrate < (wiz->startingBitrate * 75 / 100)) { - server.bitrate = (int)bitrate * 70 / 100; - } else { - server.bitrate = wiz->startingBitrate; - } - - server.ms = obs_output_get_connect_time_ms(output); - success = true; - } - - if (!success) { - QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_CONNECT_FAIL))); - return; - } - - int bestBitrate = 0; - int bestMS = 0x7FFFFFFF; - string bestServer; - string bestServerName; - - for (auto &server : servers) { - bool close = abs(server.bitrate - bestBitrate) < 400; - - if ((!close && server.bitrate > bestBitrate) || (close && server.ms < bestMS)) { - bestServer = server.address; - bestServerName = server.name; - bestBitrate = server.bitrate; - bestMS = server.ms; - } - } - - wiz->server = std::move(bestServer); - wiz->serverName = std::move(bestServerName); - wiz->idealBitrate = bestBitrate; - - QMetaObject::invokeMethod(this, "NextStage"); -} - -/* this is used to estimate the lower bitrate limit for a given - * resolution/fps. yes, it is a totally arbitrary equation that gets - * the closest to the expected values */ -static long double EstimateBitrateVal(int cx, int cy, int fps_num, int fps_den) -{ - long fps = (long double)fps_num / (long double)fps_den; - long double areaVal = pow((long double)(cx * cy), 0.85l); - return areaVal * sqrt(pow(fps, 1.1l)); -} - -static long double EstimateMinBitrate(int cx, int cy, int fps_num, int fps_den) -{ - long double val = EstimateBitrateVal(1920, 1080, 60, 1) / 5800.0l; - return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; -} - -static long double EstimateUpperBitrate(int cx, int cy, int fps_num, int fps_den) -{ - long double val = EstimateBitrateVal(1280, 720, 30, 1) / 3000.0l; - return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; -} - -struct Result { - int cx; - int cy; - int fps_num; - int fps_den; - - inline Result(int cx_, int cy_, int fps_num_, int fps_den_) - : cx(cx_), - cy(cy_), - fps_num(fps_num_), - fps_den(fps_den_) - { - } -}; - -static void CalcBaseRes(int &baseCX, int &baseCY) -{ - const int maxBaseArea = 1920 * 1200; - const int clipResArea = 1920 * 1080; - - /* if base resolution unusually high, recalculate to a more reasonable - * value to start the downscaling at, based upon 1920x1080's area. - * - * for 16:9 resolutions this will always change the starting value to - * 1920x1080 */ - if ((baseCX * baseCY) > maxBaseArea) { - long double xyAspect = (long double)baseCX / (long double)baseCY; - baseCY = (int)sqrt((long double)clipResArea / xyAspect); - baseCX = (int)((long double)baseCY * xyAspect); - } -} - -bool AutoConfigTestPage::TestSoftwareEncoding() -{ - TestMode testMode; - QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral(""))); - - /* -----------------------------------*/ - /* create obs objects */ - - OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr); - OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSOutputAutoRelease output = obs_output_create("null_output", "null", nullptr, nullptr); - - /* -----------------------------------*/ - /* configure settings */ - - OBSDataAutoRelease aencoder_settings = obs_data_create(); - OBSDataAutoRelease vencoder_settings = obs_data_create(); - obs_data_set_int(aencoder_settings, "bitrate", 32); - - if (wiz->type != AutoConfig::Type::Recording) { - obs_data_set_int(vencoder_settings, "keyint_sec", 2); - obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); - obs_data_set_string(vencoder_settings, "rate_control", "CBR"); - obs_data_set_string(vencoder_settings, "profile", "main"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - } else { - obs_data_set_int(vencoder_settings, "crf", 20); - obs_data_set_string(vencoder_settings, "rate_control", "CRF"); - obs_data_set_string(vencoder_settings, "profile", "high"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - } - - /* -----------------------------------*/ - /* apply settings */ - - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); - - /* -----------------------------------*/ - /* connect encoders/services/outputs */ - - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); - - /* -----------------------------------*/ - /* connect signals */ - - auto on_stopped = [&]() { - unique_lock lock(m); - cv.notify_one(); - }; - - using on_stopped_t = decltype(on_stopped); - - auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = *reinterpret_cast(data); - on_stopped(); - }; - - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "deactivate", pre_on_stopped, &on_stopped); - - /* -----------------------------------*/ - /* calculate starting resolution */ - - int baseCX = wiz->baseResolutionCX; - int baseCY = wiz->baseResolutionCY; - CalcBaseRes(baseCX, baseCY); - - /* -----------------------------------*/ - /* calculate starting test rates */ - - int pcores = os_get_physical_cores(); - int lcores = os_get_logical_cores(); - int maxDataRate; - if (lcores > 8 || pcores > 4) { - /* superb */ - maxDataRate = 1920 * 1200 * 60 + 1000; - - } else if (lcores > 4 && pcores == 4) { - /* great */ - maxDataRate = 1920 * 1080 * 60 + 1000; - - } else if (pcores == 4) { - /* okay */ - maxDataRate = 1920 * 1080 * 30 + 1000; - - } else { - /* toaster */ - maxDataRate = 960 * 540 * 30 + 1000; - } - - /* -----------------------------------*/ - /* perform tests */ - - vector results; - int i = 0; - int count = 1; - - auto testRes = [&](int cy, int fps_num, int fps_den, bool force) { - int per = ++i * 100 / count; - QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); - - if (cy > baseCY) - return true; - - /* no need for more than 3 tests max */ - if (results.size() >= 3) - return true; - - if (!fps_num || !fps_den) { - fps_num = wiz->specificFPSNum; - fps_den = wiz->specificFPSDen; - } - - long double fps = ((long double)fps_num / (long double)fps_den); - - int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy); - - if (!force && wiz->type != AutoConfig::Type::Recording) { - int est = EstimateMinBitrate(cx, cy, fps_num, fps_den); - if (est > wiz->idealBitrate) - return true; - } - - long double rate = (long double)cx * (long double)cy * fps; - if (!force && rate > maxDataRate) - return true; - - testMode.SetVideo(cx, cy, fps_num, fps_den); - - obs_encoder_set_video(vencoder, obs_get_video()); - obs_encoder_set_audio(aencoder, obs_get_audio()); - obs_encoder_update(vencoder, vencoder_settings); - - obs_output_set_media(output, obs_get_video(), obs_get_audio()); - - QString cxStr = QString::number(cx); - QString cyStr = QString::number(cy); - - QString fpsStr = (fps_den > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2); - - QMetaObject::invokeMethod(this, "UpdateMessage", - Q_ARG(QString, QTStr(TEST_RES_VAL).arg(cxStr, cyStr, fpsStr))); - - unique_lock ul(m); - if (cancel) - return false; - - if (!obs_output_start(output)) { - QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_RES_FAIL))); - return false; - } - - cv.wait_for(ul, chrono::seconds(5)); - - obs_output_stop(output); - cv.wait(ul); - - int skipped = (int)video_output_get_skipped_frames(obs_get_video()); - if (force || skipped <= 10) - results.emplace_back(cx, cy, fps_num, fps_den); - - return !cancel; - }; - - if (wiz->specificFPSNum && wiz->specificFPSDen) { - count = 7; - if (!testRes(2160, 0, 0, false)) - return false; - if (!testRes(1440, 0, 0, false)) - return false; - if (!testRes(1080, 0, 0, false)) - return false; - if (!testRes(720, 0, 0, false)) - return false; - if (!testRes(480, 0, 0, false)) - return false; - if (!testRes(360, 0, 0, false)) - return false; - if (!testRes(240, 0, 0, true)) - return false; - } else { - count = 14; - if (!testRes(2160, 60, 1, false)) - return false; - if (!testRes(2160, 30, 1, false)) - return false; - if (!testRes(1440, 60, 1, false)) - return false; - if (!testRes(1440, 30, 1, false)) - return false; - if (!testRes(1080, 60, 1, false)) - return false; - if (!testRes(1080, 30, 1, false)) - return false; - if (!testRes(720, 60, 1, false)) - return false; - if (!testRes(720, 30, 1, false)) - return false; - if (!testRes(480, 60, 1, false)) - return false; - if (!testRes(480, 30, 1, false)) - return false; - if (!testRes(360, 60, 1, false)) - return false; - if (!testRes(360, 30, 1, false)) - return false; - if (!testRes(240, 60, 1, false)) - return false; - if (!testRes(240, 30, 1, true)) - return false; - } - - /* -----------------------------------*/ - /* find preferred settings */ - - int minArea = 960 * 540 + 1000; - - if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { - Result &result1 = results[0]; - Result &result2 = results[1]; - - if (result1.fps_num == 30 && result2.fps_num == 60) { - int nextArea = result2.cx * result2.cy; - if (nextArea >= minArea) - results.erase(results.begin()); - } - } - - Result result = results.front(); - wiz->idealResolutionCX = result.cx; - wiz->idealResolutionCY = result.cy; - wiz->idealFPSNum = result.fps_num; - wiz->idealFPSDen = result.fps_den; - - long double fUpperBitrate = EstimateUpperBitrate(result.cx, result.cy, result.fps_num, result.fps_den); - - int upperBitrate = int(floor(fUpperBitrate / 50.0l) * 50.0l); - - if (wiz->streamingEncoder != AutoConfig::Encoder::x264) { - upperBitrate *= 114; - upperBitrate /= 100; - } - - if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful && - !wiz->multitrackVideo.bitrate.has_value()) - wiz->multitrackVideo.bitrate = wiz->idealBitrate; - - if (wiz->idealBitrate > upperBitrate) - wiz->idealBitrate = upperBitrate; - - softwareTested = true; - return true; -} - -void AutoConfigTestPage::FindIdealHardwareResolution() -{ - int baseCX = wiz->baseResolutionCX; - int baseCY = wiz->baseResolutionCY; - CalcBaseRes(baseCX, baseCY); - - vector results; - - int pcores = os_get_physical_cores(); - int maxDataRate; - if (pcores >= 4) { - maxDataRate = 1920 * 1200 * 60 + 1000; - } else { - maxDataRate = 1280 * 720 * 30 + 1000; - } - - auto testRes = [&](int cy, int fps_num, int fps_den, bool force) { - if (cy > baseCY) - return; - - if (results.size() >= 3) - return; - - if (!fps_num || !fps_den) { - fps_num = wiz->specificFPSNum; - fps_den = wiz->specificFPSDen; - } - - long double fps = ((long double)fps_num / (long double)fps_den); - - int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy); - - long double rate = (long double)cx * (long double)cy * fps; - if (!force && rate > maxDataRate) - return; - - AutoConfig::Encoder encType = wiz->streamingEncoder; - bool nvenc = encType == AutoConfig::Encoder::NVENC; - - int minBitrate = EstimateMinBitrate(cx, cy, fps_num, fps_den); - - /* most hardware encoders don't have a good quality to bitrate - * ratio, so increase the minimum bitrate estimate for them. - * NVENC currently is the exception because of the improvements - * its made to its quality in recent generations. */ - if (!nvenc) - minBitrate = minBitrate * 114 / 100; - - if (wiz->type == AutoConfig::Type::Recording) - force = true; - if (force || wiz->idealBitrate >= minBitrate) - results.emplace_back(cx, cy, fps_num, fps_den); - }; - - if (wiz->specificFPSNum && wiz->specificFPSDen) { - testRes(2160, 0, 0, false); - testRes(1440, 0, 0, false); - testRes(1080, 0, 0, false); - testRes(720, 0, 0, false); - testRes(480, 0, 0, false); - testRes(360, 0, 0, false); - testRes(240, 0, 0, true); - } else { - testRes(2160, 60, 1, false); - testRes(2160, 30, 1, false); - testRes(1440, 60, 1, false); - testRes(1440, 30, 1, false); - testRes(1080, 60, 1, false); - testRes(1080, 30, 1, false); - testRes(720, 60, 1, false); - testRes(720, 30, 1, false); - testRes(480, 60, 1, false); - testRes(480, 30, 1, false); - testRes(360, 60, 1, false); - testRes(360, 30, 1, false); - testRes(240, 60, 1, false); - testRes(240, 30, 1, true); - } - - int minArea = 960 * 540 + 1000; - - if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { - Result &result1 = results[0]; - Result &result2 = results[1]; - - if (result1.fps_num == 30 && result2.fps_num == 60) { - int nextArea = result2.cx * result2.cy; - if (nextArea >= minArea) - results.erase(results.begin()); - } - } - - Result result = results.front(); - wiz->idealResolutionCX = result.cx; - wiz->idealResolutionCY = result.cy; - wiz->idealFPSNum = result.fps_num; - wiz->idealFPSDen = result.fps_den; -} - -void AutoConfigTestPage::TestStreamEncoderThread() -{ - bool preferHardware = wiz->preferHardware; - if (!softwareTested) { - if (!preferHardware || !wiz->hardwareEncodingAvailable) { - if (!TestSoftwareEncoding()) { - return; - } - } - } - - if (!softwareTested) { - if (wiz->nvencAvailable) - wiz->streamingEncoder = AutoConfig::Encoder::NVENC; - else if (wiz->qsvAvailable) - wiz->streamingEncoder = AutoConfig::Encoder::QSV; - else if (wiz->appleAvailable) - wiz->streamingEncoder = AutoConfig::Encoder::Apple; - else - wiz->streamingEncoder = AutoConfig::Encoder::AMD; - } else { - wiz->streamingEncoder = AutoConfig::Encoder::x264; - } - -#ifdef __linux__ - // On linux CBR rate control is not guaranteed so fallback to x264. - if (wiz->streamingEncoder == AutoConfig::Encoder::QSV) { - wiz->streamingEncoder = AutoConfig::Encoder::x264; - if (!TestSoftwareEncoding()) { - return; - } - } -#endif - - if (preferHardware && !softwareTested && wiz->hardwareEncodingAvailable) - FindIdealHardwareResolution(); - - QMetaObject::invokeMethod(this, "NextStage"); -} - -void AutoConfigTestPage::TestRecordingEncoderThread() -{ - if (!wiz->hardwareEncodingAvailable && !softwareTested) { - if (!TestSoftwareEncoding()) { - return; - } - } - - if (wiz->type == AutoConfig::Type::Recording && wiz->hardwareEncodingAvailable) - FindIdealHardwareResolution(); - - wiz->recordingQuality = AutoConfig::Quality::High; - - bool recordingOnly = wiz->type == AutoConfig::Type::Recording; - - if (wiz->hardwareEncodingAvailable) { - if (wiz->nvencAvailable) - wiz->recordingEncoder = AutoConfig::Encoder::NVENC; - else if (wiz->qsvAvailable) - wiz->recordingEncoder = AutoConfig::Encoder::QSV; - else if (wiz->appleAvailable) - wiz->recordingEncoder = AutoConfig::Encoder::Apple; - else - wiz->recordingEncoder = AutoConfig::Encoder::AMD; - } else { - wiz->recordingEncoder = AutoConfig::Encoder::x264; - } - - if (wiz->recordingEncoder != AutoConfig::Encoder::NVENC) { - if (!recordingOnly) { - wiz->recordingEncoder = AutoConfig::Encoder::Stream; - wiz->recordingQuality = AutoConfig::Quality::Stream; - } - } - - QMetaObject::invokeMethod(this, "NextStage"); -} - -#define ENCODER_TEXT(x) "Basic.Settings.Output.Simple.Encoder." x -#define ENCODER_SOFTWARE ENCODER_TEXT("Software") -#define ENCODER_NVENC ENCODER_TEXT("Hardware.NVENC.H264") -#define ENCODER_QSV ENCODER_TEXT("Hardware.QSV.H264") -#define ENCODER_AMD ENCODER_TEXT("Hardware.AMD.H264") -#define ENCODER_APPLE ENCODER_TEXT("Hardware.Apple.H264") - -#define QUALITY_SAME "Basic.Settings.Output.Simple.RecordingQuality.Stream" -#define QUALITY_HIGH "Basic.Settings.Output.Simple.RecordingQuality.Small" - -void set_closest_res(int &cx, int &cy, struct obs_service_resolution *res_list, size_t count) -{ - int best_pixel_diff = 0x7FFFFFFF; - int start_cx = cx; - int start_cy = cy; - - for (size_t i = 0; i < count; i++) { - struct obs_service_resolution &res = res_list[i]; - int pixel_cx_diff = abs(start_cx - res.cx); - int pixel_cy_diff = abs(start_cy - res.cy); - int pixel_diff = pixel_cx_diff + pixel_cy_diff; - - if (pixel_diff < best_pixel_diff) { - best_pixel_diff = pixel_diff; - cx = res.cx; - cy = res.cy; - } - } -} - -void AutoConfigTestPage::FinalizeResults() -{ - ui->stackedWidget->setCurrentIndex(1); - setSubTitle(QTStr(SUBTITLE_COMPLETE)); - - QFormLayout *form = results; - - auto encName = [](AutoConfig::Encoder enc) -> QString { - switch (enc) { - case AutoConfig::Encoder::x264: - return QTStr(ENCODER_SOFTWARE); - case AutoConfig::Encoder::NVENC: - return QTStr(ENCODER_NVENC); - case AutoConfig::Encoder::QSV: - return QTStr(ENCODER_QSV); - case AutoConfig::Encoder::AMD: - return QTStr(ENCODER_AMD); - case AutoConfig::Encoder::Apple: - return QTStr(ENCODER_APPLE); - case AutoConfig::Encoder::Stream: - return QTStr(QUALITY_SAME); - } - - return QTStr(ENCODER_SOFTWARE); - }; - - auto newLabel = [this](const char *str) -> QLabel * { - return new QLabel(QTStr(str), this); - }; - - if (wiz->type == AutoConfig::Type::Streaming) { - const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common"; - - OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", nullptr, nullptr); - - OBSDataAutoRelease service_settings = obs_data_create(); - OBSDataAutoRelease vencoder_settings = obs_data_create(); - - if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful && - !wiz->multitrackVideo.bitrate.has_value()) - wiz->multitrackVideo.bitrate = wiz->idealBitrate; - - obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); - - obs_data_set_string(service_settings, "service", wiz->serviceName.c_str()); - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, nullptr); - - BPtr res_list; - size_t res_count; - int maxFPS; - obs_service_get_supported_resolutions(service, &res_list, &res_count); - obs_service_get_max_fps(service, &maxFPS); - - if (res_list) { - set_closest_res(wiz->idealResolutionCX, wiz->idealResolutionCY, res_list, res_count); - } - if (maxFPS) { - double idealFPS = (double)wiz->idealFPSNum / (double)wiz->idealFPSDen; - if (idealFPS > (double)maxFPS) { - wiz->idealFPSNum = maxFPS; - wiz->idealFPSDen = 1; - } - } - - wiz->idealBitrate = (int)obs_data_get_int(vencoder_settings, "bitrate"); - - if (!wiz->customServer) - form->addRow(newLabel("Basic.AutoConfig.StreamPage.Service"), - new QLabel(wiz->serviceName.c_str(), ui->finishPage)); - form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), - new QLabel(wiz->serverName.c_str(), ui->finishPage)); - form->addRow(newLabel("Basic.Settings.Stream.MultitrackVideoLabel"), - newLabel(wiz->multitrackVideo.testSuccessful ? "Yes" : "No")); - - if (wiz->multitrackVideo.testSuccessful) { - form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), newLabel("Automatic")); - form->addRow(newLabel(TEST_RESULT_SE), newLabel("Automatic")); - form->addRow(newLabel("Basic.AutoConfig.TestPage.Result.StreamingResolution"), - newLabel("Automatic")); - } else { - form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), - new QLabel(QString::number(wiz->idealBitrate), ui->finishPage)); - form->addRow(newLabel(TEST_RESULT_SE), - new QLabel(encName(wiz->streamingEncoder), ui->finishPage)); - } - } - - QString baseRes = - QString("%1x%2").arg(QString::number(wiz->baseResolutionCX), QString::number(wiz->baseResolutionCY)); - QString scaleRes = - QString("%1x%2").arg(QString::number(wiz->idealResolutionCX), QString::number(wiz->idealResolutionCY)); - - if (wiz->recordingEncoder != AutoConfig::Encoder::Stream || - wiz->recordingQuality != AutoConfig::Quality::Stream) - form->addRow(newLabel(TEST_RESULT_RE), new QLabel(encName(wiz->recordingEncoder), ui->finishPage)); - - QString recQuality; - - switch (wiz->recordingQuality) { - case AutoConfig::Quality::High: - recQuality = QTStr(QUALITY_HIGH); - break; - case AutoConfig::Quality::Stream: - recQuality = QTStr(QUALITY_SAME); - break; - } - - form->addRow(newLabel("Basic.Settings.Output.Simple.RecordingQuality"), new QLabel(recQuality, ui->finishPage)); - - long double fps = (long double)wiz->idealFPSNum / (long double)wiz->idealFPSDen; - - QString fpsStr = (wiz->idealFPSDen > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2); - - form->addRow(newLabel("Basic.Settings.Video.BaseResolution"), new QLabel(baseRes, ui->finishPage)); - form->addRow(newLabel("Basic.Settings.Video.ScaledResolution"), new QLabel(scaleRes, ui->finishPage)); - form->addRow(newLabel("Basic.Settings.Video.FPS"), new QLabel(fpsStr, ui->finishPage)); - - // FIXME: form layout is super squished, probably need to set proper sizepolicy on all widgets? -} - -#define STARTING_SEPARATOR "\n==== Auto-config wizard testing commencing ======\n" -#define STOPPING_SEPARATOR "\n==== Auto-config wizard testing stopping ========\n" - -void AutoConfigTestPage::NextStage() -{ - if (testThread.joinable()) - testThread.join(); - if (cancel) - return; - - ui->subProgressLabel->setText(QString()); - - /* make it skip to bandwidth stage if only set to config recording */ - if (stage == Stage::Starting) { - if (!started) { - blog(LOG_INFO, STARTING_SEPARATOR); - started = true; - } - - if (wiz->type != AutoConfig::Type::Streaming) { - stage = Stage::StreamEncoder; - } else if (!wiz->bandwidthTest) { - stage = Stage::BandwidthTest; - } - } - - if (stage == Stage::Starting) { - stage = Stage::BandwidthTest; - StartBandwidthStage(); - - } else if (stage == Stage::BandwidthTest) { - stage = Stage::StreamEncoder; - StartStreamEncoderStage(); - - } else if (stage == Stage::StreamEncoder) { - stage = Stage::RecordingEncoder; - StartRecordingEncoderStage(); - - } else { - stage = Stage::Finished; - FinalizeResults(); - emit completeChanged(); - } -} - -void AutoConfigTestPage::UpdateMessage(QString message) -{ - ui->subProgressLabel->setText(message); -} - -void AutoConfigTestPage::Failure(QString message) -{ - ui->errorLabel->setText(message); - ui->stackedWidget->setCurrentIndex(2); -} - -void AutoConfigTestPage::Progress(int percentage) -{ - ui->progressBar->setValue(percentage); -} - -AutoConfigTestPage::AutoConfigTestPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigTestPage) -{ - ui->setupUi(this); - setTitle(QTStr("Basic.AutoConfig.TestPage")); - setSubTitle(QTStr(SUBTITLE_TESTING)); - setCommitPage(true); -} - -AutoConfigTestPage::~AutoConfigTestPage() -{ - if (testThread.joinable()) { - { - unique_lock ul(m); - cancel = true; - cv.notify_one(); - } - testThread.join(); - } - - if (started) - blog(LOG_INFO, STOPPING_SEPARATOR); -} - -void AutoConfigTestPage::initializePage() -{ - if (wiz->type == AutoConfig::Type::VirtualCam) { - wiz->idealResolutionCX = wiz->baseResolutionCX; - wiz->idealResolutionCY = wiz->baseResolutionCY; - wiz->idealFPSNum = 30; - wiz->idealFPSDen = 1; - stage = Stage::Finished; - } else { - stage = Stage::Starting; - } - - setSubTitle(QTStr(SUBTITLE_TESTING)); - softwareTested = false; - cancel = false; - DeleteLayout(results); - results = new QFormLayout(); - results->setContentsMargins(0, 0, 0, 0); - ui->finishPageLayout->insertLayout(1, results); - ui->stackedWidget->setCurrentIndex(0); - NextStage(); -} - -void AutoConfigTestPage::cleanupPage() -{ - if (testThread.joinable()) { - { - unique_lock ul(m); - cancel = true; - cv.notify_one(); - } - testThread.join(); - } -} - -bool AutoConfigTestPage::isComplete() const -{ - return stage == Stage::Finished; -} - -int AutoConfigTestPage::nextId() const -{ - return -1; -} From bcc6880183782e9999eb43e7ed90ec1a5982db0e Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 19:19:27 +0100 Subject: [PATCH 26/37] frontend: Prepare main application implementation for splits --- UI/obs-app.cpp => frontend/OBSApp.cpp | 0 UI/obs-app.hpp => frontend/OBSApp.hpp | 0 .../OBSApp_Themes.cpp | 0 .../OBSStudioAPI.cpp | 0 frontend/OBSStudioAPI.hpp | 636 ++++ frontend/obs-main.cpp | 2662 +++++++++++++++++ frontend/utility/BaseLexer.hpp | 280 ++ .../utility/OBSTheme.hpp | 0 frontend/utility/OBSThemeVariable.hpp | 66 + frontend/utility/OBSTranslator.cpp | 2662 +++++++++++++++++ frontend/utility/OBSTranslator.hpp | 280 ++ 11 files changed, 6586 insertions(+) rename UI/obs-app.cpp => frontend/OBSApp.cpp (100%) rename UI/obs-app.hpp => frontend/OBSApp.hpp (100%) rename UI/obs-app-theming.cpp => frontend/OBSApp_Themes.cpp (100%) rename UI/api-interface.cpp => frontend/OBSStudioAPI.cpp (100%) create mode 100644 frontend/OBSStudioAPI.hpp create mode 100644 frontend/obs-main.cpp create mode 100644 frontend/utility/BaseLexer.hpp rename UI/obs-app-theming.hpp => frontend/utility/OBSTheme.hpp (100%) create mode 100644 frontend/utility/OBSThemeVariable.hpp create mode 100644 frontend/utility/OBSTranslator.cpp create mode 100644 frontend/utility/OBSTranslator.hpp diff --git a/UI/obs-app.cpp b/frontend/OBSApp.cpp similarity index 100% rename from UI/obs-app.cpp rename to frontend/OBSApp.cpp diff --git a/UI/obs-app.hpp b/frontend/OBSApp.hpp similarity index 100% rename from UI/obs-app.hpp rename to frontend/OBSApp.hpp diff --git a/UI/obs-app-theming.cpp b/frontend/OBSApp_Themes.cpp similarity index 100% rename from UI/obs-app-theming.cpp rename to frontend/OBSApp_Themes.cpp diff --git a/UI/api-interface.cpp b/frontend/OBSStudioAPI.cpp similarity index 100% rename from UI/api-interface.cpp rename to frontend/OBSStudioAPI.cpp diff --git a/frontend/OBSStudioAPI.hpp b/frontend/OBSStudioAPI.hpp new file mode 100644 index 000000000..bb3715017 --- /dev/null +++ b/frontend/OBSStudioAPI.hpp @@ -0,0 +1,636 @@ +#include +#include +#include "obs-app.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" + +#include + +using namespace std; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSource); + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +extern volatile bool streaming_active; +extern volatile bool recording_active; +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; +extern volatile bool virtualcam_active; + +/* ------------------------------------------------------------------------- */ + +template struct OBSStudioCallback { + T callback; + void *private_data; + + inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} +}; + +template +inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) +{ + for (size_t i = 0; i < callbacks.size(); i++) { + OBSStudioCallback curCB = callbacks[i]; + if (curCB.callback == callback && curCB.private_data == private_data) + return i; + } + + return (size_t)-1; +} + +struct OBSStudioAPI : obs_frontend_callbacks { + OBSBasic *main; + vector> callbacks; + vector> saveCallbacks; + vector> preloadCallbacks; + + inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} + + void *obs_frontend_get_main_window(void) override { return (void *)main; } + + void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); } + + void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); } + + void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override + { + for (int i = 0; i < main->ui->scenes->count(); i++) { + QListWidgetItem *item = main->ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + obs_source_t *source = obs_scene_get_source(scene); + + if (obs_source_get_ref(source) != nullptr) + da_push_back(sources->sources, &source); + } + } + + obs_source_t *obs_frontend_get_current_scene(void) override + { + if (main->IsPreviewProgramMode()) { + return obs_weak_source_get_source(main->programScene); + } else { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } + } + + void obs_frontend_set_current_scene(obs_source_t *scene) override + { + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene))); + } else { + QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); + } + } + + void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override + { + for (int i = 0; i < main->ui->transitions->count(); i++) { + OBSSource tr = main->ui->transitions->itemData(i).value(); + + if (!tr) + continue; + + if (obs_source_get_ref(tr) != nullptr) + da_push_back(sources->sources, &tr); + } + } + + obs_source_t *obs_frontend_get_current_transition(void) override + { + OBSSource tr = main->GetCurrentTransition(); + return obs_source_get_ref(tr); + } + + void obs_frontend_set_current_transition(obs_source_t *transition) override + { + QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); + } + + int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); } + + void obs_frontend_set_transition_duration(int duration) override + { + QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); + } + + void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); } + + void obs_frontend_set_tbar_position(int position) override + { + QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); + } + + int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); } + + void obs_frontend_get_scene_collections(std::vector &strings) override + { + for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { + strings.emplace_back(collectionName); + } + } + + char *obs_frontend_get_current_scene_collection(void) override + { + const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); + return bstrdup(currentCollection.name.c_str()); + } + + void obs_frontend_set_current_scene_collection(const char *collection) override + { + QList menuActions = main->ui->sceneCollectionMenu->actions(); + QString qstrCollection = QT_UTF8(collection); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrCollection) { + action->trigger(); + break; + } + } + } + } + + bool obs_frontend_add_scene_collection(const char *name) override + { + bool success = false; + QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), + Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name))); + return success; + } + + void obs_frontend_get_profiles(std::vector &strings) override + { + const OBSProfileCache &profiles = main->GetProfileCache(); + + for (auto &[profileName, profile] : profiles) { + strings.emplace_back(profileName); + } + } + + char *obs_frontend_get_current_profile(void) override + { + const OBSProfile &profile = main->GetCurrentProfile(); + return bstrdup(profile.name.c_str()); + } + + char *obs_frontend_get_current_profile_path(void) override + { + const OBSProfile &profile = main->GetCurrentProfile(); + + return bstrdup(profile.path.u8string().c_str()); + } + + void obs_frontend_set_current_profile(const char *profile) override + { + QList menuActions = main->ui->profileMenu->actions(); + QString qstrProfile = QT_UTF8(profile); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrProfile) { + action->trigger(); + break; + } + } + } + } + + void obs_frontend_create_profile(const char *name) override + { + QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); + } + + void obs_frontend_duplicate_profile(const char *name) override + { + QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); + } + + void obs_frontend_delete_profile(const char *profile) override + { + QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); + } + + void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); } + + void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); } + + bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); } + + void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); } + + void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); } + + bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); } + + void obs_frontend_recording_pause(bool pause) override + { + QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); + } + + bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); } + + bool obs_frontend_recording_split_file(void) override + { + if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + uint8_t stack[128]; + calldata cd; + calldata_init_fixed(&cd, stack, sizeof(stack)); + proc_handler_call(ph, "split_file", &cd); + bool result = calldata_bool(&cd, "split_file_enabled"); + return result; + } else { + return false; + } + } + + bool obs_frontend_recording_add_chapter(const char *name) override + { + if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) + return false; + + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + + calldata cd; + calldata_init(&cd); + calldata_set_string(&cd, "chapter_name", name); + bool result = proc_handler_call(ph, "add_chapter", &cd); + calldata_free(&cd); + return result; + } + + void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); } + + void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); } + + void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); } + + bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); } + + void *obs_frontend_add_tools_menu_qaction(const char *name) override + { + main->ui->menuTools->setEnabled(true); + return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); + } + + void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override + { + main->ui->menuTools->setEnabled(true); + + auto func = [private_data, callback]() { + callback(private_data); + }; + + QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); + QObject::connect(action, &QAction::triggered, func); + } + + void *obs_frontend_add_dock(void *dock) override + { + QDockWidget *d = reinterpret_cast(dock); + + QString name = d->objectName(); + if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { + blog(LOG_WARNING, "The object name of the added dock is empty or already used," + " a temporary one will be set to avoid conflicts"); + + char *uuid = os_generate_uuid(); + name = QT_UTF8(uuid); + bfree(uuid); + name.append("_oldExtraDock"); + + d->setObjectName(name); + } + + return (void *)main->AddDockWidget(d); + } + + bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override + { + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + OBSDock *dock = new OBSDock(main); + dock->setWidget((QWidget *)widget); + dock->setWindowTitle(QT_UTF8(title)); + dock->setObjectName(QT_UTF8(id)); + + main->AddDockWidget(dock, Qt::RightDockWidgetArea); + + dock->setVisible(false); + dock->setFloating(true); + + return true; + } + + void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); } + + bool obs_frontend_add_custom_qdock(const char *id, void *dock) override + { + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + QDockWidget *d = reinterpret_cast(dock); + d->setObjectName(QT_UTF8(id)); + + main->AddCustomDockWidget(d); + + return true; + } + + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + callbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + callbacks.erase(callbacks.begin() + idx); + } + + obs_output_t *obs_frontend_get_streaming_output(void) override + { + auto multitrackVideo = main->outputHandler->multitrackVideo.get(); + auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; + if (mtvOutput) + return mtvOutput; + + OBSOutput output = main->outputHandler->streamOutput.Get(); + return obs_output_get_ref(output); + } + + obs_output_t *obs_frontend_get_recording_output(void) override + { + OBSOutput out = main->outputHandler->fileOutput.Get(); + return obs_output_get_ref(out); + } + + obs_output_t *obs_frontend_get_replay_buffer_output(void) override + { + OBSOutput out = main->outputHandler->replayBuffer.Get(); + return obs_output_get_ref(out); + } + + config_t *obs_frontend_get_profile_config(void) override { return main->activeConfiguration; } + + config_t *obs_frontend_get_global_config(void) override + { + blog(LOG_WARNING, + "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); + return App()->GetAppConfig(); + } + + config_t *obs_frontend_get_app_config(void) override { return App()->GetAppConfig(); } + + config_t *obs_frontend_get_user_config(void) override { return App()->GetUserConfig(); } + + void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) override + { + SavedProjectorInfo proj = { + ProjectorType::Preview, + monitor, + geometry ? geometry : "", + name ? name : "", + }; + if (type) { + if (astrcmpi(type, "Source") == 0) + proj.type = ProjectorType::Source; + else if (astrcmpi(type, "Scene") == 0) + proj.type = ProjectorType::Scene; + else if (astrcmpi(type, "StudioProgram") == 0) + proj.type = ProjectorType::StudioProgram; + else if (astrcmpi(type, "Multiview") == 0) + proj.type = ProjectorType::Multiview; + } + QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), + Q_ARG(SavedProjectorInfo *, &proj)); + } + + void obs_frontend_save(void) override { main->SaveProject(); } + + void obs_frontend_defer_save_begin(void) override { QMetaObject::invokeMethod(main, "DeferSaveBegin"); } + + void obs_frontend_defer_save_end(void) override { QMetaObject::invokeMethod(main, "DeferSaveEnd"); } + + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + saveCallbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + saveCallbacks.erase(saveCallbacks.begin() + idx); + } + + void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + preloadCallbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + preloadCallbacks.erase(preloadCallbacks.begin() + idx); + } + + void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override + { + App()->PushUITranslation(translate); + } + + void obs_frontend_pop_ui_translation(void) override { App()->PopUITranslation(); } + + void obs_frontend_set_streaming_service(obs_service_t *service) override { main->SetService(service); } + + obs_service_t *obs_frontend_get_streaming_service(void) override { return main->GetService(); } + + void obs_frontend_save_streaming_service(void) override { main->SaveService(); } + + bool obs_frontend_preview_program_mode_active(void) override { return main->IsPreviewProgramMode(); } + + void obs_frontend_set_preview_program_mode(bool enable) override { main->SetPreviewProgramMode(enable); } + + void obs_frontend_preview_program_trigger_transition(void) override + { + QMetaObject::invokeMethod(main, "TransitionClicked"); + } + + bool obs_frontend_preview_enabled(void) override { return main->previewEnabled; } + + void obs_frontend_set_preview_enabled(bool enable) override + { + if (main->previewEnabled != enable) + main->EnablePreviewDisplay(enable); + } + + obs_source_t *obs_frontend_get_current_preview_scene(void) override + { + if (main->IsPreviewProgramMode()) { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } + + return nullptr; + } + + void obs_frontend_set_current_preview_scene(obs_source_t *scene) override + { + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), + Q_ARG(bool, false)); + } + } + + void obs_frontend_take_screenshot(void) override { QMetaObject::invokeMethod(main, "Screenshot"); } + + void obs_frontend_take_source_screenshot(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); + } + + obs_output_t *obs_frontend_get_virtualcam_output(void) override + { + OBSOutput output = main->outputHandler->virtualCam.Get(); + return obs_output_get_ref(output); + } + + void obs_frontend_start_virtualcam(void) override { QMetaObject::invokeMethod(main, "StartVirtualCam"); } + + void obs_frontend_stop_virtualcam(void) override { QMetaObject::invokeMethod(main, "StopVirtualCam"); } + + bool obs_frontend_virtualcam_active(void) override { return os_atomic_load_bool(&virtualcam_active); } + + void obs_frontend_reset_video(void) override { main->ResetVideo(); } + + void obs_frontend_open_source_properties(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_source_filters(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_source_interaction(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override + { + QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); + } + + char *obs_frontend_get_current_record_output_path(void) override + { + const char *recordOutputPath = main->GetCurrentOutputPath(); + + return bstrdup(recordOutputPath); + } + + const char *obs_frontend_get_locale_string(const char *string) override { return Str(string); } + + bool obs_frontend_is_theme_dark(void) override { return App()->IsThemeDark(); } + + char *obs_frontend_get_last_recording(void) override + { + return bstrdup(main->outputHandler->lastRecordingPath.c_str()); + } + + char *obs_frontend_get_last_screenshot(void) override { return bstrdup(main->lastScreenshot.c_str()); } + + char *obs_frontend_get_last_replay(void) override { return bstrdup(main->lastReplay.c_str()); } + + void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, + const char *undo_data, const char *redo_data, bool repeatable) override + { + main->undo_s.add_action( + name, [undo](const std::string &data) { undo(data.c_str()); }, + [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); + } + + void on_load(obs_data_t *settings) override + { + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } + } + + void on_preload(obs_data_t *settings) override + { + for (size_t i = preloadCallbacks.size(); i > 0; i--) { + auto cb = preloadCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } + } + + void on_save(obs_data_t *settings) override + { + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, true, cb.private_data); + } + } + + void on_event(enum obs_frontend_event event) override + { + if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && + event != OBS_FRONTEND_EVENT_EXIT) + return; + + for (size_t i = callbacks.size(); i > 0; i--) { + auto cb = callbacks[i - 1]; + cb.callback(event, cb.private_data); + } + } +}; + +obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) +{ + obs_frontend_callbacks *api = new OBSStudioAPI(main); + obs_frontend_set_callbacks_internal(api); + return api; +} diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp new file mode 100644 index 000000000..883bee21a --- /dev/null +++ b/frontend/obs-main.cpp @@ -0,0 +1,2662 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "obs-proxy-style.hpp" +#include "log-viewer.hpp" +#include "volume-control.hpp" +#include "window-basic-main.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-basic-settings.hpp" +#include "platform.hpp" + +#include + +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include "update/models/branches.hpp" +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#include +#endif + +#include + +#include "ui-config.h" + +using namespace std; + +static log_handler_t def_log_handler; + +static string currentLogFile; +static string lastLogFile; +static string lastCrashLogFile; + +bool portable_mode = false; +bool steam = false; +bool safe_mode = false; +bool disable_3p_plugins = false; +bool unclean_shutdown = false; +bool disable_shutdown_check = false; +static bool multi = false; +static bool log_verbose = false; +static bool unfiltered_log = false; +bool opt_start_streaming = false; +bool opt_start_recording = false; +bool opt_studio_mode = false; +bool opt_start_replaybuffer = false; +bool opt_start_virtualcam = false; +bool opt_minimize_tray = false; +bool opt_allow_opengl = false; +bool opt_always_on_top = false; +bool opt_disable_updater = false; +bool opt_disable_missing_files_check = false; +string opt_starting_collection; +string opt_starting_profile; +string opt_starting_scene; + +bool restart = false; +bool restart_safe = false; +QStringList arguments; + +QPointer obsLogViewer; + +#ifndef _WIN32 +int OBSApp::sigintFd[2]; +#endif + +// GPU hint exports for AMD/NVIDIA laptops +#ifdef _MSC_VER +extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; +extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +#endif + +QObject *CreateShortcutFilter() +{ + return new OBSEventFilter([](QObject *obj, QEvent *event) { + auto mouse_event = [](QMouseEvent &event) { + if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) + return true; + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event.type() == QEvent::MouseButtonPress; + + switch (event.button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return false; + + case Qt::MiddleButton: + hotkey.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + hotkey.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4); + MAP_BUTTON(2, 5); + MAP_BUTTON(3, 6); + MAP_BUTTON(4, 7); + MAP_BUTTON(5, 8); + MAP_BUTTON(6, 9); + MAP_BUTTON(7, 10); + MAP_BUTTON(8, 11); + MAP_BUTTON(9, 12); + MAP_BUTTON(10, 13); + MAP_BUTTON(11, 14); + MAP_BUTTON(12, 15); + MAP_BUTTON(13, 16); + MAP_BUTTON(14, 17); + MAP_BUTTON(15, 18); + MAP_BUTTON(16, 19); + MAP_BUTTON(17, 20); + MAP_BUTTON(18, 21); + MAP_BUTTON(19, 22); + MAP_BUTTON(20, 23); + MAP_BUTTON(21, 24); + MAP_BUTTON(22, 25); + MAP_BUTTON(23, 26); + MAP_BUTTON(24, 27); +#undef MAP_BUTTON + } + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + auto key_event = [&](QKeyEvent *event) { + int key = event->key(); + bool enabledInFocus = App()->HotkeysEnabledInFocus(); + + if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) + return true; + + QDialog *dialog = qobject_cast(obj); + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event->type() == QEvent::KeyPress; + + switch (key) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + hotkey.key = obs_key_from_virtual_key(57); + pressed = true; + break; +#endif + + case Qt::Key_Enter: + case Qt::Key_Escape: + case Qt::Key_Return: + if (dialog && pressed) + return false; + if (!enabledInFocus) + return true; + /* Falls through. */ + default: + hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + if (event->isAutoRepeat()) + return true; + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + return mouse_event(*static_cast(event)); + + /*case QEvent::MouseButtonDblClick: + case QEvent::Wheel:*/ + case QEvent::KeyPress: + case QEvent::KeyRelease: + return key_event(static_cast(event)); + + default: + return false; + } + }); +} + +string CurrentTimeString() +{ + using namespace std::chrono; + + struct tm tstruct; + char buf[80]; + + auto tp = system_clock::now(); + auto now = system_clock::to_time_t(tp); + tstruct = *localtime(&now); + + size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); + if (ratio_less::value && written && (sizeof(buf) - written) > 5) { + auto tp_secs = time_point_cast(tp); + auto millis = duration_cast(tp - tp_secs).count(); + + snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); + } + + return buf; +} + +string CurrentDateTimeString() +{ + time_t now = time(0); + struct tm tstruct; + char buf[80]; + tstruct = *localtime(&now); + strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); + return buf; +} + +static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) +{ + static mutex logfile_mutex; + string msg; + msg += timeString; + msg += str; + + logfile_mutex.lock(); + logFile << msg << endl; + logfile_mutex.unlock(); + + if (!!obsLogViewer) + QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), + Q_ARG(QString, QString(msg.c_str()))); +} + +static inline void LogStringChunk(fstream &logFile, char *str, int log_level) +{ + char *nextLine = str; + string timeString = CurrentTimeString(); + timeString += ": "; + + while (*nextLine) { + char *nextLine = strchr(str, '\n'); + if (!nextLine) + break; + + if (nextLine != str && nextLine[-1] == '\r') { + nextLine[-1] = 0; + } else { + nextLine[0] = 0; + } + + LogString(logFile, timeString.c_str(), str, log_level); + nextLine++; + str = nextLine; + } + + LogString(logFile, timeString.c_str(), str, log_level); +} + +#define MAX_REPEATED_LINES 30 +#define MAX_CHAR_VARIATION (255 * 3) + +static inline int sum_chars(const char *str) +{ + int val = 0; + for (; *str != 0; str++) + val += *str; + + return val; +} + +static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) +{ + static mutex log_mutex; + static const char *last_msg_ptr = nullptr; + static int last_char_sum = 0; + static int rep_count = 0; + + int new_sum = sum_chars(output_str); + + lock_guard guard(log_mutex); + + if (unfiltered_log) { + return false; + } + + if (last_msg_ptr == msg) { + int diff = std::abs(new_sum - last_char_sum); + if (diff < MAX_CHAR_VARIATION) { + return (rep_count++ >= MAX_REPEATED_LINES); + } + } + + if (rep_count > MAX_REPEATED_LINES) { + logFile << CurrentTimeString() << ": Last log entry repeated for " + << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; + } + + last_msg_ptr = msg; + last_char_sum = new_sum; + rep_count = 0; + + return false; +} + +static void do_log(int log_level, const char *msg, va_list args, void *param) +{ + fstream &logFile = *static_cast(param); + char str[8192]; + +#ifndef _WIN32 + va_list args2; + va_copy(args2, args); +#endif + + vsnprintf(str, sizeof(str), msg, args); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (wNum > 1) { + static wstring wide_buf; + static mutex wide_mutex; + + lock_guard lock(wide_mutex); + wide_buf.reserve(wNum + 1); + wide_buf.resize(wNum - 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); + wide_buf.push_back('\n'); + + OutputDebugStringW(wide_buf.c_str()); + } + } +#endif + +#if !defined(_WIN32) && defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + + if (log_level <= LOG_INFO || log_verbose) { +#if !defined(_WIN32) && !defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + if (!too_many_repeated_entries(logFile, msg, str)) + LogStringChunk(logFile, str, log_level); + } + +#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) + if (log_level <= LOG_ERROR && IsDebuggerPresent()) + __debugbreak(); +#endif + +#ifndef _WIN32 + va_end(args2); +#endif +} + +#define DEFAULT_LANG "en-US" + +bool OBSApp::InitGlobalConfigDefaults() +{ + config_set_default_uint(appConfig, "General", "MaxLogs", 10); + config_set_default_int(appConfig, "General", "InfoIncrement", -1); + config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); + config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); + +#if _WIN32 + config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); +#else + config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); +#endif + +#ifdef _WIN32 + config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); +#endif + +#ifdef __APPLE__ + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); + config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); + config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); +#endif + + return true; +} + +bool OBSApp::InitGlobalLocationDefaults() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(NULL, "Unable to get global configuration path."); + return false; + } + + config_set_default_string(appConfig, "Locations", "Configuration", path); + config_set_default_string(appConfig, "Locations", "SceneCollections", path); + config_set_default_string(appConfig, "Locations", "Profiles", path); + + return true; +} + +void OBSApp::InitUserConfigDefaults() +{ + config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); + + config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); + + config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); + config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); + config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); + config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); + config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); + config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); + + config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); + + config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); +} + +static bool do_mkdir(const char *path) +{ + if (os_mkdirs(path) == MKDIR_ERROR) { + OBSErrorBox(NULL, "Failed to create directory %s", path); + return false; + } + + return true; +} + +static bool MakeUserDirs() +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) + return false; + if (!do_mkdir(path)) + return false; + +#ifdef _WIN32 + if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + +#ifdef WHATSNEW_ENABLED + if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + return true; +} + +constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; +constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; + +static bool MakeUserProfileDirs() +{ + const std::filesystem::path userProfilePath = + App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); + const std::filesystem::path userScenesPath = + App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); + + if (!std::filesystem::exists(userProfilePath)) { + try { + std::filesystem::create_directories(userProfilePath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create user profile directory '%s'\n%s", + userProfilePath.u8string().c_str(), error.what()); + return false; + } + } + + if (!std::filesystem::exists(userScenesPath)) { + try { + std::filesystem::create_directories(userScenesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create user scene collection directory '%s'\n%s", + userScenesPath.u8string().c_str(), error.what()); + return false; + } + } + + return true; +} + +bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) +{ + if (!layout) + return false; + + if (astrcmpi(layout, "horizontaltop") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "horizontalbottom") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalleft") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalright") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + return true; + } + + return false; +} + +bool OBSApp::InitGlobalConfig() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); + if (len <= 0) { + return false; + } + + int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); + if (errorcode != CONFIG_SUCCESS) { + OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); + return false; + } + + uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); + + if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { + bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); + + if (!migratedUserSettings) { + bool migrated = MigrateGlobalSettings(); + + config_set_bool(appConfig, "General", "Pre31Migrated", migrated); + config_save_safe(appConfig, "tmp", nullptr); + } + } + + InitGlobalConfigDefaults(); + InitGlobalLocationDefaults(); + + if (IsPortableMode()) { + userConfigLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); + } else { + userConfigLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); + } + + bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); + + return userConfigResult; +} + +bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) +{ + const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; + + int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); + + if (errorCode != CONFIG_SUCCESS) { + OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); + return false; + } + + MigrateLegacySettings(lastVersion); + InitUserConfigDefaults(); + + return true; +} + +void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) +{ + bool hasChanges = false; + + const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); + const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); + const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); + const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); + const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); + + const map defaultsMap{ + {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; + + for (auto &[version, configKey] : defaultsMap) { + if (!config_has_user_value(userConfig, "General", configKey.c_str())) { + bool useOldDefaults = lastVersion && lastVersion < version; + config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); + + hasChanges = true; + } + } + + if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { + const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); + + bool layoutUpdated = UpdatePre22MultiviewLayout(layout); + + hasChanges = hasChanges | layoutUpdated; + } + + if (lastVersion && lastVersion < v24) { + bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); + + if (disableHotkeysInFocus) { + config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); + } + + hasChanges = true; + } + + if (hasChanges) { + userConfig.SaveSafe("tmp"); + } +} + +static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; +static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; + +bool OBSApp::MigrateGlobalSettings() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(nullptr, "Unable to get global configuration path."); + return false; + } + + std::string legacyConfigFileString; + legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); + legacyConfigFileString.append(path).append(OBSGlobalIniPath); + + const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); + + std::string configFileString; + configFileString.reserve(strlen(path) + OBSUserIniPath.size()); + configFileString.append(path).append(OBSUserIniPath); + + const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); + + if (std::filesystem::exists(userConfigFile)) { + OBSErrorBox(nullptr, + "Unable to migrate global configuration - user configuration file already exists."); + return false; + } + + try { + std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); + } catch (const std::filesystem::filesystem_error &) { + OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); + return false; + } + + return true; +} + +bool OBSApp::InitLocale() +{ + ProfileScope("OBSApp::InitLocale"); + + const char *lang = config_get_string(userConfig, "General", "Language"); + bool userLocale = config_has_user_value(userConfig, "General", "Language"); + if (!userLocale || !lang || lang[0] == '\0') + lang = DEFAULT_LANG; + + locale = lang; + + // set basic default application locale + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + string englishPath; + if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { + OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); + return false; + } + + textLookup = text_lookup_create(englishPath.c_str()); + if (!textLookup) { + OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); + return false; + } + + bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; + + if (userLocale && defaultLang) + return true; + + if (!userLocale && defaultLang) { + for (auto &locale_ : GetPreferredLocales()) { + if (locale_ == lang) + return true; + + stringstream file; + file << "locale/" << locale_ << ".ini"; + + string path; + if (!GetDataFilePath(file.str().c_str(), path)) + continue; + + if (!text_lookup_add(textLookup, path.c_str())) + continue; + + blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); + locale = locale_; + + // set application default locale to the new choosen one + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + return true; + } + + return true; + } + + stringstream file; + file << "locale/" << lang << ".ini"; + + string path; + if (GetDataFilePath(file.str().c_str(), path)) { + if (!text_lookup_add(textLookup, path.c_str())) + blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); + } else { + blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); + } + + return true; +} + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) +{ + JsonBranches branches; + + try { + nlohmann::json json = nlohmann::json::parse(jsonString); + branches = json.get(); + } catch (nlohmann::json::exception &e) { + error = e.what(); + return; + } + + for (const JsonBranch &json_branch : branches) { +#ifdef _WIN32 + if (!json_branch.windows) + continue; +#elif defined(__APPLE__) + if (!json_branch.macos) + continue; +#endif + + UpdateBranch branch = { + QString::fromStdString(json_branch.name), + QString::fromStdString(json_branch.display_name), + QString::fromStdString(json_branch.description), + json_branch.enabled, + json_branch.visible, + }; + out.push_back(branch); + } +} + +bool LoadBranchesFile(vector &out) +{ + string error; + string branchesText; + + BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); + + QFile branchesFile(branchesFilePath.Get()); + if (!branchesFile.open(QIODevice::ReadOnly)) { + error = "Opening file failed."; + goto fail; + } + + branchesText = branchesFile.readAll(); + if (branchesText.empty()) { + error = "File empty."; + goto fail; + } + + ParseBranchesJson(branchesText, out, error); + if (error.empty()) + return !out.empty(); + +fail: + blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); + return false; +} +#endif + +void OBSApp::SetBranchData(const string &data) +{ +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + string error; + vector result; + + ParseBranchesJson(data, result, error); + + if (!error.empty()) { + blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); + return; + } + + if (!result.empty()) + updateBranches = result; + + branches_loaded = true; +#else + UNUSED_PARAMETER(data); +#endif +} + +std::vector OBSApp::GetBranches() +{ + vector out; + /* Always ensure the default branch exists */ + out.push_back(UpdateBranch{"stable", "", "", true, true}); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + if (!branches_loaded) { + vector result; + if (LoadBranchesFile(result)) + updateBranches = result; + + branches_loaded = true; + } +#endif + + /* Copy additional branches to result (if any) */ + if (!updateBranches.empty()) + out.insert(out.end(), updateBranches.begin(), updateBranches.end()); + + return out; +} + +OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) + : QApplication(argc, argv), + profilerNameStore(store) +{ + /* fix float handling */ +#if defined(Q_OS_UNIX) + if (!setlocale(LC_NUMERIC, "C")) + blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); +#endif + +#ifndef _WIN32 + /* Handle SIGINT properly */ + 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); +#endif + + sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); + +#ifndef __APPLE__ + setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); +#endif + + setDesktopFileName("com.obsproject.Studio"); +} + +OBSApp::~OBSApp() +{ +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(false); +#else + delete snInt; + close(sigintFd[0]); + close(sigintFd[1]); +#endif + +#ifdef __APPLE__ + bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); + bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); + if (vsyncDisabled && resetVSync) + EnableOSXVSync(true); +#endif + + os_inhibit_sleep_set_active(sleepInhibitor, false); + os_inhibit_sleep_destroy(sleepInhibitor); + + if (libobs_initialized) + obs_shutdown(); +} + +static void move_basic_to_profiles(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path profilesPath = + App()->userProfilesLocation / std::filesystem::u8path("obs-studio/basic/profiles"); + + if (std::filesystem::exists(profilesPath)) { + return; + } + + try { + std::filesystem::create_directories(profilesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create profiles directory for migration from basic profile\n%s", + error.what()); + return; + } + + const std::filesystem::path newProfilePath = profilesPath / std::filesystem::u8path(Str("Untitled")); + + for (auto &entry : std::filesystem::directory_iterator(basicPath)) { + if (entry.is_directory()) { + continue; + } + + if (entry.path().filename().u8string() == "scenes.json") { + continue; + } + + if (!std::filesystem::exists(newProfilePath)) { + try { + std::filesystem::create_directory(newProfilePath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create profile directory for 'Untitled'\n%s", error.what()); + return; + } + } + + const filesystem::path destinationFile = newProfilePath / entry.path().filename(); + + const auto copyOptions = std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(entry.path(), destinationFile, copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s", + entry.path().filename().u8string().c_str(), error.what()); + + return; + } + } +} + +static void move_basic_to_scene_collections(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path sceneCollectionPath = + App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); + + if (std::filesystem::exists(sceneCollectionPath)) { + return; + } + + try { + std::filesystem::create_directories(sceneCollectionPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create scene collection directory for migration from basic scene collection\n%s", + error.what()); + return; + } + + const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); + const std::filesystem::path destinationFile = + (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); + + try { + std::filesystem::rename(sourceFile, destinationFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); + return; + } +} + +void OBSApp::AppInit() +{ + ProfileScope("OBSApp::AppInit"); + + if (!MakeUserDirs()) + throw "Failed to create required user directories"; + if (!InitGlobalConfig()) + throw "Failed to initialize global config"; + if (!InitLocale()) + throw "Failed to load locale"; + if (!InitTheme()) + throw "Failed to load theme"; + + config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); + + if (!config_has_user_value(userConfig, "Basic", "Profile")) { + config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + } + + if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { + config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + } + +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(true); +#endif + +#ifdef __APPLE__ + if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) + EnableOSXVSync(false); +#endif + + UpdateHotkeyFocusSetting(false); + + move_basic_to_profiles(); + move_basic_to_scene_collections(); + + if (!MakeUserProfileDirs()) + throw "Failed to create profile directories"; +} + +const char *OBSApp::GetRenderModule() const +{ + const char *renderer = config_get_string(appConfig, "Video", "Renderer"); + + return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; +} + +static bool StartupOBS(const char *locale, profiler_name_store_t *store) +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + + return obs_startup(locale, path, store); +} + +inline void OBSApp::ResetHotkeyState(bool inFocus) +{ + obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); +} + +void OBSApp::UpdateHotkeyFocusSetting(bool resetState) +{ + enableHotkeysInFocus = true; + enableHotkeysOutOfFocus = true; + + const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); + + if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { + enableHotkeysInFocus = false; + } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { + enableHotkeysOutOfFocus = false; + } + + if (resetState) + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +void OBSApp::DisableHotkeys() +{ + enableHotkeysInFocus = false; + enableHotkeysOutOfFocus = false; + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +Q_DECLARE_METATYPE(VoidFunc) + +void OBSApp::Exec(VoidFunc func) +{ + func(); +} + +static void ui_task_handler(obs_task_t task, void *param, bool wait) +{ + auto doTask = [=]() { + /* to get clang-format to behave */ + task(param); + }; + QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); +} + +bool OBSApp::OBSInit() +{ + ProfileScope("OBSApp::OBSInit"); + + qRegisterMetaType("VoidFunc"); + +#if !defined(_WIN32) && !defined(__APPLE__) + if (QApplication::platformName() == "xcb") { + obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); + blog(LOG_INFO, "Using EGL/X11"); + } + +#ifdef ENABLE_WAYLAND + if (QApplication::platformName().contains("wayland")) { + obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + blog(LOG_INFO, "Platform: Wayland"); + } +#endif + + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); +#endif + +#ifdef __APPLE__ + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#endif + + if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) + return false; + + libobs_initialized = true; + + obs_set_ui_task_handler(ui_task_handler); + +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); + obs_apply_private_data(settings); + + blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); + + blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); +#endif +#ifdef _WIN32 + bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); + blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); +#endif + + blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); + blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); + + if (safe_mode) { + blog(LOG_WARNING, "Safe Mode enabled."); + } else if (disable_3p_plugins) { + blog(LOG_WARNING, "Third-party plugins disabled."); + } + + setQuitOnLastWindowClosed(false); + + mainWindow = new OBSBasic(); + + mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); + connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); + + mainWindow->OBSInit(); + + connect(this, &QGuiApplication::applicationStateChanged, + [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); + ResetHotkeyState(applicationState() == Qt::ApplicationActive); + return true; +} + +string OBSApp::GetVersionString(bool platform) const +{ + stringstream ver; + +#ifdef HAVE_OBSCONFIG_H + ver << obs_get_version_string(); +#else + ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; + +#endif + + if (platform) { + ver << " ("; +#ifdef _WIN32 + if (sizeof(void *) == 8) + ver << "64-bit, "; + else + ver << "32-bit, "; + + ver << "windows)"; +#elif __APPLE__ + ver << "mac)"; +#elif __OpenBSD__ + ver << "openbsd)"; +#elif __FreeBSD__ + ver << "freebsd)"; +#else /* assume linux for the time being */ + ver << "linux)"; +#endif + } + + return ver.str(); +} + +bool OBSApp::IsPortableMode() +{ + return portable_mode; +} + +bool OBSApp::IsUpdaterDisabled() +{ + return opt_disable_updater; +} + +bool OBSApp::IsMissingFilesCheckDisabled() +{ + return opt_disable_missing_files_check; +} + +#ifdef __APPLE__ +#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" +#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" +#elif _WIN32 +#define INPUT_AUDIO_SOURCE "wasapi_input_capture" +#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" +#else +#define INPUT_AUDIO_SOURCE "pulse_input_capture" +#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" +#endif + +const char *OBSApp::InputAudioSource() const +{ + return INPUT_AUDIO_SOURCE; +} + +const char *OBSApp::OutputAudioSource() const +{ + return OUTPUT_AUDIO_SOURCE; +} + +const char *OBSApp::GetLastLog() const +{ + return lastLogFile.c_str(); +} + +const char *OBSApp::GetCurrentLog() const +{ + return currentLogFile.c_str(); +} + +const char *OBSApp::GetLastCrashLog() const +{ + return lastCrashLogFile.c_str(); +} + +bool OBSApp::TranslateString(const char *lookupVal, const char **out) const +{ + for (obs_frontend_translate_ui_cb cb : translatorHooks) { + if (cb(lookupVal, out)) + return true; + } + + return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); +} + +// Global handler to receive all QEvent::Show events so we can apply +// display affinity on any newly created windows and dialogs without +// caring where they are coming from (e.g. plugins). +bool OBSApp::notify(QObject *receiver, QEvent *e) +{ + QWidget *w; + QWindow *window; + int windowType; + + if (!receiver->isWidgetType()) + goto skip; + + if (e->type() != QEvent::Show) + goto skip; + + w = qobject_cast(receiver); + + if (!w->isWindow()) + goto skip; + + window = w->windowHandle(); + if (!window) + goto skip; + + windowType = window->flags() & Qt::WindowType::WindowType_Mask; + + if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || + windowType == Qt::WindowType::Tool) { + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->SetDisplayAffinity(window); + } + +skip: + return QApplication::notify(receiver, e); +} + +QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const +{ + const char *out = nullptr; + QString str(sourceText); + str.replace(" ", ""); + if (!App()->TranslateString(QT_TO_UTF8(str), &out)) + return QString(sourceText); + + return QT_UTF8(out); +} + +static bool get_token(lexer *lex, string &str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + str.assign(token.text.array, token.text.len); + return true; +} + +static bool expect_token(lexer *lex, const char *str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + return strref_cmp(&token.text, str) == 0; +} + +static uint64_t convert_log_name(bool has_prefix, const char *name) +{ + BaseLexer lex; + string year, month, day, hour, minute, second; + + lexer_start(lex, name); + + if (has_prefix) { + string temp; + if (!get_token(lex, temp, BASETOKEN_ALPHA)) + return 0; + } + + if (!get_token(lex, year, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, month, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, day, BASETOKEN_DIGIT)) + return 0; + if (!get_token(lex, hour, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, minute, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, second, BASETOKEN_DIGIT)) + return 0; + + stringstream timestring; + timestring << year << month << day << hour << minute << second; + return std::stoull(timestring.str()); +} + +/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ +/* TODO: Remove after version 32.0. */ +#if defined(__FreeBSD__) +static void move_to_xdg(void) +{ + char old_path[512]; + char new_path[512]; + char *home = getenv("HOME"); + if (!home) + return; + + if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) + return; + + /* make base xdg path if it doesn't already exist */ + if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) + return; + if (os_mkdirs(new_path) == MKDIR_ERROR) + return; + + if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) + return; + + if (os_file_exists(old_path) && !os_file_exists(new_path)) { + rename(old_path, new_path); + } +} +#endif + +static void delete_oldest_file(bool has_prefix, const char *location) +{ + BPtr logDir(GetAppConfigPathPtr(location)); + string oldestLog; + uint64_t oldest_ts = (uint64_t)-1; + struct os_dirent *entry; + + unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); + + os_dir_t *dir = os_opendir(logDir); + if (dir) { + unsigned int count = 0; + + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts) { + if (ts < oldest_ts) { + oldestLog = entry->d_name; + oldest_ts = ts; + } + + count++; + } + } + + os_closedir(dir); + + if (count > maxLogs) { + stringstream delPath; + + delPath << logDir << "/" << oldestLog; + os_unlink(delPath.str().c_str()); + } + } +} + +static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) +{ + BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); + struct os_dirent *entry; + os_dir_t *dir = os_opendir(logDir); + uint64_t highest_ts = 0; + + if (dir) { + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts > highest_ts) { + last = entry->d_name; + highest_ts = ts; + } + } + + os_closedir(dir); + } +} + +string GenerateTimeDateFilename(const char *extension, bool noSpace) +{ + time_t now = time(0); + char file[256] = {}; + struct tm *cur_time; + + cur_time = localtime(&now); + snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, + cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, + extension); + + return string(file); +} + +string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) +{ + BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); + return string(filename); +} + +static void FindBestFilename(string &strPath, bool noSpace) +{ + int num = 2; + + if (!os_file_exists(strPath.c_str())) + return; + + const char *ext = strrchr(strPath.c_str(), '.'); + if (!ext) + return; + + int extStart = int(ext - strPath.c_str()); + for (;;) { + string testPath = strPath; + string numStr; + + numStr = noSpace ? "_" : " ("; + numStr += to_string(num++); + if (!noSpace) + numStr += ")"; + + testPath.insert(extStart, numStr); + + if (!os_file_exists(testPath.c_str())) { + strPath = testPath; + break; + } + } +} + +static void ensure_directory_exists(string &path) +{ + replace(path.begin(), path.end(), '\\', '/'); + + size_t last = path.rfind('/'); + if (last == string::npos) + return; + + string directory = path.substr(0, last); + os_mkdirs(directory.c_str()); +} + +static void remove_reserved_file_characters(string &s) +{ + replace(s.begin(), s.end(), '\\', '/'); + replace(s.begin(), s.end(), '*', '_'); + replace(s.begin(), s.end(), '?', '_'); + replace(s.begin(), s.end(), '"', '_'); + replace(s.begin(), s.end(), '|', '_'); + replace(s.begin(), s.end(), ':', '_'); + replace(s.begin(), s.end(), '>', '_'); + replace(s.begin(), s.end(), '<', '_'); +} + +string GetFormatString(const char *format, const char *prefix, const char *suffix) +{ + string f; + + f = format; + + if (prefix && *prefix) { + string str_prefix = prefix; + + if (str_prefix.back() != ' ') + str_prefix += " "; + + size_t insert_pos = 0; + size_t tmp; + + tmp = f.find_last_of('/'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + tmp = f.find_last_of('\\'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + f.insert(insert_pos, str_prefix); + } + + if (suffix && *suffix) { + if (*suffix != ' ') + f += " "; + f += suffix; + } + + remove_reserved_file_characters(f); + + return f; +} + +string GetFormatExt(const char *container) +{ + string ext = container; + if (ext == "fragmented_mp4") + ext = "mp4"; + if (ext == "hybrid_mp4") + ext = "mp4"; + else if (ext == "fragmented_mov") + ext = "mov"; + else if (ext == "hls") + ext = "m3u8"; + else if (ext == "mpegts") + ext = "ts"; + + return ext; +} + +string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; + + if (!dir) { + if (main->isVisible()) + OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); + return ""; + } + + os_closedir(dir); + + string strPath; + strPath += path; + + char lastChar = strPath.back(); + if (lastChar != '/' && lastChar != '\\') + strPath += "/"; + + string ext = GetFormatExt(container); + strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); + ensure_directory_exists(strPath); + if (!overwrite) + FindBestFilename(strPath, noSpace); + + return strPath; +} + +vector> GetLocaleNames() +{ + string path; + if (!GetDataFilePath("locale.ini", path)) + throw "Could not find locale.ini path"; + + ConfigFile ini; + if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) + throw "Could not open locale.ini"; + + size_t sections = config_num_sections(ini); + + vector> names; + names.reserve(sections); + for (size_t i = 0; i < sections; i++) { + const char *tag = config_get_section(ini, i); + const char *name = config_get_string(ini, tag, "Name"); + names.emplace_back(tag, name); + } + + return names; +} + +static void create_log_file(fstream &logFile) +{ + stringstream dst; + + get_last_log(false, "obs-studio/logs", lastLogFile); +#ifdef _WIN32 + get_last_log(true, "obs-studio/crashes", lastCrashLogFile); +#endif + + currentLogFile = GenerateTimeDateFilename("txt"); + dst << "obs-studio/logs/" << currentLogFile.c_str(); + + BPtr path(GetAppConfigPathPtr(dst.str().c_str())); + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); +#else + logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); +#endif + + if (logFile.is_open()) { + delete_oldest_file(false, "obs-studio/logs"); + base_set_log_handler(do_log, &logFile); + } else { + blog(LOG_ERROR, "Failed to open log file"); + } +} + +static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { + profiler_name_store_free(store); +}; + +using ProfilerNameStore = std::unique_ptr; + +ProfilerNameStore CreateNameStore() +{ + return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; +} + +static auto SnapshotRelease = [](profiler_snapshot_t *snap) { + profile_snapshot_free(snap); +}; + +using ProfilerSnapshot = std::unique_ptr; + +ProfilerSnapshot GetSnapshot() +{ + return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; +} + +static void SaveProfilerData(const ProfilerSnapshot &snap) +{ + if (currentLogFile.empty()) + return; + + auto pos = currentLogFile.rfind('.'); + if (pos == currentLogFile.npos) + return; + +#define LITERAL_SIZE(x) x, (sizeof(x) - 1) + ostringstream dst; + dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); + dst.write(currentLogFile.c_str(), pos); + dst.write(LITERAL_SIZE(".csv.gz")); +#undef LITERAL_SIZE + + BPtr path = GetAppConfigPathPtr(dst.str().c_str()); + if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) + blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); +} + +static auto ProfilerFree = [](void *) { + profiler_stop(); + + auto snap = GetSnapshot(); + + profiler_print(snap.get()); + profiler_print_time_between_calls(snap.get()); + + SaveProfilerData(snap); + + profiler_free(); +}; + +QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) + return new VolumeAccessibleInterface(static_cast(object)); + + return nullptr; +} + +static const char *run_program_init = "run_program_init"; +static int run_program(fstream &logFile, int argc, char *argv[]) +{ + int ret = -1; + + auto profilerNameStore = CreateNameStore(); + + std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); + + profiler_start(); + profile_register_root(run_program_init, 0); + + ScopeProfiler prof{run_program_init}; + +#ifdef _WIN32 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + + QCoreApplication::addLibraryPath("."); + +#if __APPLE__ + InstallNSApplicationSubclass(); + InstallNSThreadLocks(); + + if (!isInBundle()) { + blog(LOG_ERROR, + "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); + return ret; + } +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) + /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and + * crashes loading saved geometry. Just turn off this theme and let users complain OBS + * looks ugly instead of crashing. */ + const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); + if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) + unsetenv("QT_QPA_PLATFORMTHEME"); +#endif + + /* NOTE: This disables an optimisation in Qt that attempts to determine if + * any "siblings" intersect with a widget when determining the approximate + * visible/unobscured area. However, by Qt's own admission this is slow + * and in the case of OBS it significantly slows down lists with many + * elements (e.g. Hotkeys) and it is actually faster to disable it. */ + qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); + + OBSApp program(argc, argv, profilerNameStore.get()); + try { + QAccessible::installFactory(accessibleFactory); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + + bool created_log = false; + + program.AppInit(); + delete_oldest_file(false, "obs-studio/profiler_data"); + + OBSTranslator translator; + program.installTranslator(&translator); + + /* --------------------------------------- */ + /* check and warn if already running */ + + bool cancel_launch = false; + bool already_running = false; + +#ifdef _WIN32 + RunOnceMutex rom = +#endif + CheckIfAlreadyRunning(already_running); + + if (!already_running) { + goto run; + } + + if (!multi) { + QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), + QTStr("AlreadyRunning.Text")); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); + QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); + mb.setDefaultButton(cancelButton); + + mb.exec(); + cancel_launch = mb.clickedButton() == cancelButton; + } + + if (cancel_launch) + return 0; + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { + blog(LOG_INFO, "User enabled --multi flag and is now " + "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); + /* Clear unclean_shutdown flag as multiple instances + * running from the same config will lead to a + * false-positive detection.*/ + unclean_shutdown = false; + } + + /* --------------------------------------- */ + run: + +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) + // Mounted by termina during chromeOS linux container startup + // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh + os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); + if (crosDir) { + QMessageBox::StandardButtons buttons(QMessageBox::Ok); + QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, + nullptr); + + mb.exec(); + return 0; + } +#endif + + if (!created_log) + create_log_file(logFile); + + if (unclean_shutdown) { + blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); + } + + if (unclean_shutdown && !safe_mode) { + QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); + QPushButton *launchSafeButton = + mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); + QPushButton *launchNormalButton = + mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); + mb.setDefaultButton(launchNormalButton); + mb.exec(); + + safe_mode = mb.clickedButton() == launchSafeButton; + if (safe_mode) { + blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); + } else { + blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); + } + } + + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { + switch (type) { +#ifdef _DEBUG + case QtDebugMsg: + blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); + break; + case QtInfoMsg: + blog(LOG_INFO, "%s", QT_TO_UTF8(message)); + break; +#else + case QtDebugMsg: + case QtInfoMsg: + break; +#endif + case QtWarningMsg: + blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); + break; + case QtCriticalMsg: + case QtFatalMsg: + blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); + break; + } + }); + +#ifdef __APPLE__ + MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); + MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); + MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); + MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); + + int permissionsDialogLastShown = + config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); + if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { + OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, + accessibility_permission); + check.exec(); + } +#endif + +#ifdef _WIN32 + if (IsRunningOnWine()) { + QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); + mb.setTextFormat(Qt::RichText); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); + QPushButton *closeButton = mb.addButton(QMessageBox::Close); + mb.setDefaultButton(closeButton); + + mb.exec(); + if (mb.clickedButton() == closeButton) + return 0; + } +#endif + + if (argc > 1) { + stringstream stor; + stor << argv[1]; + for (int i = 2; i < argc; ++i) { + stor << " " << argv[i]; + } + blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); + } + + if (!program.OBSInit()) + return 0; + + prof.Stop(); + + ret = program.exec(); + + } catch (const char *error) { + blog(LOG_ERROR, "%s", error); + OBSErrorBox(nullptr, "%s", error); + } + + if (restart || restart_safe) { + arguments = qApp->arguments(); + + if (restart_safe) { + arguments.append("--safe-mode"); + } else { + arguments.removeAll("--safe-mode"); + } + } + + return ret; +} + +#define MAX_CRASH_REPORT_SIZE (150 * 1024) + +#ifdef _WIN32 + +#define CRASH_MESSAGE \ + "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ + "to the clipboard? The crash log will still be saved to:\n\n%s" + +static void main_crash_handler(const char *format, va_list args, void * /* param */) +{ + char *text = new char[MAX_CRASH_REPORT_SIZE]; + + vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); + text[MAX_CRASH_REPORT_SIZE - 1] = 0; + + string crashFilePath = "obs-studio/crashes"; + + delete_oldest_file(true, crashFilePath.c_str()); + + string name = crashFilePath + "/"; + name += "Crash " + GenerateTimeDateFilename("txt"); + + BPtr path(GetAppConfigPathPtr(name.c_str())); + + fstream file; + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#else + file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#endif + file << text; + file.close(); + + string pathString(path.Get()); + +#ifdef _WIN32 + std::replace(pathString.begin(), pathString.end(), '/', '\\'); +#endif + + string absolutePath = canonical(filesystem::path(pathString)).u8string(); + + size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); + + unique_ptr message_buffer(new char[size + 1]); + + snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); + + string finalMessage = string(message_buffer.get(), message_buffer.get() + size); + + int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); + + if (ret == IDYES) { + size_t len = strlen(text); + + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(mem), text, len); + GlobalUnlock(mem); + + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + + exit(-1); +} + +static void load_debug_privilege(void) +{ + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { + return; + } + + if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); + } + + if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + blog(LOG_INFO, "Could not set privilege to " + "increase GPU priority"); + } + } + + CloseHandle(token); +} +#endif + +#ifdef __APPLE__ +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#define CONFIG_PATH BASE_PATH "/config" + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + +int GetAppConfigPath(char *path, size_t size, const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + if (name && *name) { + return snprintf(path, size, CONFIG_PATH "/%s", name); + } else { + return snprintf(path, size, CONFIG_PATH); + } + } else { + return os_get_config_path(path, size, name); + } +#else + return os_get_config_path(path, size, name); +#endif +} + +char *GetAppConfigPathPtr(const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + char path[512]; + + if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { + return bstrdup(path); + } else { + return NULL; + } + } else { + return os_get_config_path_ptr(name); + } +#else + return os_get_config_path_ptr(name); +#endif +} + +int GetProgramDataPath(char *path, size_t size, const char *name) +{ + return os_get_program_data_path(path, size, name); +} + +char *GetProgramDataPathPtr(const char *name) +{ + return os_get_program_data_path_ptr(name); +} + +bool GetFileSafeName(const char *name, std::string &file) +{ + size_t base_len = strlen(name); + size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); + std::wstring wfile; + + if (!len) + return false; + + wfile.resize(len); + os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); + + for (size_t i = wfile.size(); i > 0; i--) { + size_t im1 = i - 1; + + if (iswspace(wfile[im1])) { + wfile[im1] = '_'; + } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { + wfile.erase(im1, 1); + } + } + + if (wfile.size() == 0) + wfile = L"characters_only"; + + len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); + if (!len) + return false; + + file.resize(len); + os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); + return true; +} + +bool GetClosestUnusedFileName(std::string &path, const char *extension) +{ + size_t len = path.size(); + if (extension) { + path += "."; + path += extension; + } + + if (!os_file_exists(path.c_str())) + return true; + + int index = 1; + + do { + path.resize(len); + path += std::to_string(++index); + if (extension) { + path += "."; + path += extension; + } + } while (os_file_exists(path.c_str())); + + return true; +} + +bool WindowPositionValid(QRect rect) +{ + for (QScreen *screen : QGuiApplication::screens()) { + if (screen->availableGeometry().intersects(rect)) + return true; + } + return false; +} + +static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) +{ + return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); +} + +static void check_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + /* Safe Mode detection is disabled in Debug builds to keep developers + * somewhat sane. */ + return; +#else + if (disable_shutdown_check) + return; + + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + if (os_file_exists(sentinelPath)) { + unclean_shutdown = true; + return; + } + + os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); +#endif +} + +static void delete_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + return; +#else + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + os_unlink(sentinelPath); +#endif +} + +#ifndef _WIN32 +void OBSApp::SigIntSignalHandler(int s) +{ + /* Handles SIGINT and writes to a socket. Qt will read + * from the socket in the main thread event loop and trigger + * a call to the ProcessSigInt slot, where we can safely run + * shutdown code without signal safety issues. */ + UNUSED_PARAMETER(s); + + char a = 1; + send(sigintFd[0], &a, sizeof(a), 0); +} +#endif + +void OBSApp::ProcessSigInt(void) +{ + /* This looks weird, but we can't ifdef a Qt slot function so + * the SIGINT handler simply does nothing on Windows. */ +#ifndef _WIN32 + char tmp; + recv(sigintFd[1], &tmp, sizeof(tmp), 0); + + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->close(); +#endif +} + +#ifdef _WIN32 +void OBSApp::commitData(QSessionManager &manager) +{ + if (auto main = App()->GetMainWindow()) { + QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); + manager.cancel(); + } +} +#endif + +#ifdef _WIN32 +static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; +static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " + "Redistributables.\n\nYou will now be directed to the download page."; +static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; + +static bool vc_runtime_outdated() +{ + win_version_info ver; + if (!get_dll_ver(L"msvcp140.dll", &ver)) + return true; + /* Major is always 14 (hence 140.dll), so we only care about minor. */ + if (ver.minor >= 40) + return false; + + int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); + if (choice == IDOK) { + /* Open the URL in the default browser. */ + ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); + } + + return true; +} +#endif + +int main(int argc, char *argv[]) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); + + struct sigaction sig_handler; + + sig_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sig_handler.sa_mask); + sig_handler.sa_flags = 0; + + sigaction(SIGINT, &sig_handler, NULL); + + /* Block SIGPIPE in all threads, this can happen if a thread calls write on + a closed pipe. */ + sigset_t sigpipe_mask; + sigemptyset(&sigpipe_mask); + sigaddset(&sigpipe_mask, SIGPIPE); + sigset_t saved_mask; + if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { + perror("pthread_sigmask"); + exit(1); + } +#endif + +#ifdef _WIN32 + // Abort as early as possible if MSVC runtime is outdated + if (vc_runtime_outdated()) + return 1; + // Try to keep this as early as possible + install_dll_blocklist_hook(); + + obs_init_win32_crash_handler(); + SetErrorMode(SEM_FAILCRITICALERRORS); + load_debug_privilege(); + base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } +#endif + + base_get_log_handler(&def_log_handler, nullptr); + +#if defined(__FreeBSD__) + move_to_xdg(); +#endif + + obs_set_cmdline_args(argc, argv); + + for (int i = 1; i < argc; i++) { + if (arg_is(argv[i], "--multi", "-m")) { + multi = true; + disable_shutdown_check = true; + +#if ALLOW_PORTABLE_MODE + } else if (arg_is(argv[i], "--portable", "-p")) { + portable_mode = true; + +#endif + } else if (arg_is(argv[i], "--verbose", nullptr)) { + log_verbose = true; + + } else if (arg_is(argv[i], "--safe-mode", nullptr)) { + safe_mode = true; + + } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { + disable_3p_plugins = true; + + } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { + /* This exists mostly to bypass the dialog during development. */ + disable_shutdown_check = true; + + } else if (arg_is(argv[i], "--always-on-top", nullptr)) { + opt_always_on_top = true; + + } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { + unfiltered_log = true; + + } else if (arg_is(argv[i], "--startstreaming", nullptr)) { + opt_start_streaming = true; + + } else if (arg_is(argv[i], "--startrecording", nullptr)) { + opt_start_recording = true; + + } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { + opt_start_replaybuffer = true; + + } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { + opt_start_virtualcam = true; + + } else if (arg_is(argv[i], "--collection", nullptr)) { + if (++i < argc) + opt_starting_collection = argv[i]; + + } else if (arg_is(argv[i], "--profile", nullptr)) { + if (++i < argc) + opt_starting_profile = argv[i]; + + } else if (arg_is(argv[i], "--scene", nullptr)) { + if (++i < argc) + opt_starting_scene = argv[i]; + + } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { + opt_minimize_tray = true; + + } else if (arg_is(argv[i], "--studio-mode", nullptr)) { + opt_studio_mode = true; + + } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { + opt_allow_opengl = true; + + } else if (arg_is(argv[i], "--disable-updater", nullptr)) { + opt_disable_updater = true; + + } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { + opt_disable_missing_files_check = true; + + } else if (arg_is(argv[i], "--steam", nullptr)) { + steam = true; + + } else if (arg_is(argv[i], "--help", "-h")) { + std::string help = + "--help, -h: Get list of available commands.\n\n" + "--startstreaming: Automatically start streaming.\n" + "--startrecording: Automatically start recording.\n" + "--startreplaybuffer: Start replay buffer.\n" + "--startvirtualcam: Start virtual camera (if available).\n\n" + "--collection : Use specific scene collection." + "\n" + "--profile : Use specific profile.\n" + "--scene : Start with specific scene.\n\n" + "--studio-mode: Enable studio mode.\n" + "--minimize-to-tray: Minimize to system tray.\n" +#if ALLOW_PORTABLE_MODE + "--portable, -p: Use portable mode.\n" +#endif + "--multi, -m: Don't warn when launching multiple instances.\n\n" + "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" + "--only-bundled-plugins: Only load included (first-party) plugins\n" + "--disable-shutdown-check: Disable unclean shutdown detection.\n" + "--verbose: Make log more verbose.\n" + "--always-on-top: Start in 'always on top' mode.\n\n" + "--unfiltered_log: Make log unfiltered.\n\n" + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" + "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + +#ifdef _WIN32 + MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); +#else + std::cout << help << "--version, -V: Get current version.\n"; +#endif + exit(0); + + } else if (arg_is(argv[i], "--version", "-V")) { + std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; + exit(0); + } + } + +#if ALLOW_PORTABLE_MODE + if (!portable_mode) { + portable_mode = os_file_exists(BASE_PATH "/portable_mode") || + os_file_exists(BASE_PATH "/obs_portable_mode") || + os_file_exists(BASE_PATH "/portable_mode.txt") || + os_file_exists(BASE_PATH "/obs_portable_mode.txt"); + } + + if (!opt_disable_updater) { + opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || + os_file_exists(BASE_PATH "/disable_updater.txt"); + } + + if (!opt_disable_missing_files_check) { + opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || + os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); + } +#endif + + check_safe_mode_sentinel(); + + fstream logFile; + + curl_global_init(CURL_GLOBAL_ALL); + int ret = run_program(logFile, argc, argv); + +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } + + log_blocked_dlls(); +#endif + + delete_safe_mode_sentinel(); + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); + base_set_log_handler(nullptr, nullptr); + + if (restart || restart_safe) { + auto executable = arguments.takeFirst(); + QProcess::startDetached(executable, arguments); + } + + return ret; +} diff --git a/frontend/utility/BaseLexer.hpp b/frontend/utility/BaseLexer.hpp new file mode 100644 index 000000000..dea5e530b --- /dev/null +++ b/frontend/utility/BaseLexer.hpp @@ -0,0 +1,280 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include + +#ifndef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-main.hpp" +#include "obs-app-theming.hpp" + +std::string CurrentTimeString(); +std::string CurrentDateTimeString(); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); + +struct BaseLexer { + lexer lex; + +public: + inline BaseLexer() { lexer_init(&lex); } + inline ~BaseLexer() { lexer_free(&lex); } + operator lexer *() { return &lex; } +}; + +class OBSTranslator : public QTranslator { + Q_OBJECT + +public: + virtual bool isEmpty() const override { return false; } + + virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, + int n) const override; +}; + +typedef std::function VoidFunc; + +struct UpdateBranch { + QString name; + QString display_name; + QString description; + bool is_enabled; + bool is_visible; +}; + +class OBSApp : public QApplication { + Q_OBJECT + +private: + std::string locale; + + ConfigFile appConfig; + ConfigFile userConfig; + TextLookup textLookup; + QPointer mainWindow; + profiler_name_store_t *profilerNameStore = nullptr; + std::vector updateBranches; + bool branches_loaded = false; + + bool libobs_initialized = false; + + os_inhibit_t *sleepInhibitor = nullptr; + int sleepInhibitRefs = 0; + + bool enableHotkeysInFocus = true; + bool enableHotkeysOutOfFocus = true; + + std::deque translatorHooks; + + bool UpdatePre22MultiviewLayout(const char *layout); + + bool InitGlobalConfig(); + bool InitGlobalConfigDefaults(); + bool InitGlobalLocationDefaults(); + + bool MigrateGlobalSettings(); + void MigrateLegacySettings(uint32_t lastVersion); + + bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); + void InitUserConfigDefaults(); + + bool InitLocale(); + bool InitTheme(); + + inline void ResetHotkeyState(bool inFocus); + + QPalette defaultPalette; + OBSTheme *currentTheme = nullptr; + QHash themes; + QPointer themeWatcher; + + void FindThemes(); + + bool notify(QObject *receiver, QEvent *e) override; + +#ifndef _WIN32 + static int sigintFd[2]; + QSocketNotifier *snInt = nullptr; +#else +private slots: + void commitData(QSessionManager &manager); +#endif + +private slots: + void themeFileChanged(const QString &); + +public: + OBSApp(int &argc, char **argv, profiler_name_store_t *store); + ~OBSApp(); + + void AppInit(); + bool OBSInit(); + + void UpdateHotkeyFocusSetting(bool reset = true); + void DisableHotkeys(); + + inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } + + inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } + + inline config_t *GetAppConfig() const { return appConfig; } + inline config_t *GetUserConfig() const { return userConfig; } + std::filesystem::path userConfigLocation; + std::filesystem::path userScenesLocation; + std::filesystem::path userProfilesLocation; + + inline const char *GetLocale() const { return locale.c_str(); } + + OBSTheme *GetTheme() const { return currentTheme; } + QList GetThemes() const { return themes.values(); } + OBSTheme *GetTheme(const QString &name); + bool SetTheme(const QString &name); + bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } + + void SetBranchData(const std::string &data); + std::vector GetBranches(); + + inline lookup_t *GetTextLookup() const { return textLookup; } + + inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } + + bool TranslateString(const char *lookupVal, const char **out) const; + + profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } + + const char *GetLastLog() const; + const char *GetCurrentLog() const; + + const char *GetLastCrashLog() const; + + std::string GetVersionString(bool platform = true) const; + bool IsPortableMode(); + bool IsUpdaterDisabled(); + bool IsMissingFilesCheckDisabled(); + + const char *InputAudioSource() const; + const char *OutputAudioSource() const; + + const char *GetRenderModule() const; + + inline void IncrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs++ == 0) + os_inhibit_sleep_set_active(sleepInhibitor, true); + } + + inline void DecrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs == 0) + return; + if (--sleepInhibitRefs == 0) + os_inhibit_sleep_set_active(sleepInhibitor, false); + } + + inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } + + inline void PopUITranslation() { translatorHooks.pop_front(); } +#ifndef _WIN32 + static void SigIntSignalHandler(int); +#endif + +public slots: + void Exec(VoidFunc func); + void ProcessSigInt(); + +signals: + void StyleChanged(); +}; + +int GetAppConfigPath(char *path, size_t size, const char *name); +char *GetAppConfigPathPtr(const char *name); + +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + +inline OBSApp *App() +{ + return static_cast(qApp); +} + +std::vector> GetLocaleNames(); +inline const char *Str(const char *lookup) +{ + return App()->GetString(lookup); +} +inline QString QTStr(const char *lookupVal) +{ + return QString::fromUtf8(Str(lookupVal)); +} + +bool GetFileSafeName(const char *name, std::string &file); +bool GetClosestUnusedFileName(std::string &path, const char *extension); +bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); + +bool WindowPositionValid(QRect rect); + +extern bool portable_mode; +extern bool steam; +extern bool safe_mode; +extern bool disable_3p_plugins; + +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; +extern bool opt_minimize_tray; +extern bool opt_studio_mode; +extern bool opt_allow_opengl; +extern bool opt_always_on_top; +extern std::string opt_starting_scene; +extern bool restart; +extern bool restart_safe; + +#ifdef _WIN32 +extern "C" void install_dll_blocklist_hook(void); +extern "C" void log_blocked_dlls(void); +#endif diff --git a/UI/obs-app-theming.hpp b/frontend/utility/OBSTheme.hpp similarity index 100% rename from UI/obs-app-theming.hpp rename to frontend/utility/OBSTheme.hpp diff --git a/frontend/utility/OBSThemeVariable.hpp b/frontend/utility/OBSThemeVariable.hpp new file mode 100644 index 000000000..cbaed7f24 --- /dev/null +++ b/frontend/utility/OBSThemeVariable.hpp @@ -0,0 +1,66 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + 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 + +#include + +struct OBSThemeVariable; + +struct OBSTheme { + /* internal name, must be unique */ + QString id; + QString name; + QString author; + QString extends; + + /* First ancestor base theme */ + QString parent; + /* Dependencies from root to direct ancestor */ + QStringList dependencies; + /* File path */ + std::filesystem::path location; + std::filesystem::path filename; /* Filename without extension */ + + bool isDark; + bool isVisible; /* Whether it should be shown to the user */ + bool isBaseTheme; /* Whether it is a "style" or variant */ + bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ +}; + +struct OBSThemeVariable { + enum VariableType { + Color, /* RGB color value*/ + Size, /* Number with suffix denoting size (e.g. px, pt, em) */ + Number, /* Number without suffix */ + String, /* Raw string (e.g. color name, border style, etc.) */ + Alias, /* Points at another variable, value will be the key */ + Calc, /* Simple calculation with two operands */ + }; + + /* Whether the variable should be editable in the UI */ + bool editable = false; + /* Used for VariableType::Size only */ + QString suffix; + + VariableType type; + QString name; + QVariant value; + QVariant userValue; /* If overwritten by user, use this value instead */ +}; diff --git a/frontend/utility/OBSTranslator.cpp b/frontend/utility/OBSTranslator.cpp new file mode 100644 index 000000000..883bee21a --- /dev/null +++ b/frontend/utility/OBSTranslator.cpp @@ -0,0 +1,2662 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "obs-proxy-style.hpp" +#include "log-viewer.hpp" +#include "volume-control.hpp" +#include "window-basic-main.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-basic-settings.hpp" +#include "platform.hpp" + +#include + +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include "update/models/branches.hpp" +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#include +#endif + +#include + +#include "ui-config.h" + +using namespace std; + +static log_handler_t def_log_handler; + +static string currentLogFile; +static string lastLogFile; +static string lastCrashLogFile; + +bool portable_mode = false; +bool steam = false; +bool safe_mode = false; +bool disable_3p_plugins = false; +bool unclean_shutdown = false; +bool disable_shutdown_check = false; +static bool multi = false; +static bool log_verbose = false; +static bool unfiltered_log = false; +bool opt_start_streaming = false; +bool opt_start_recording = false; +bool opt_studio_mode = false; +bool opt_start_replaybuffer = false; +bool opt_start_virtualcam = false; +bool opt_minimize_tray = false; +bool opt_allow_opengl = false; +bool opt_always_on_top = false; +bool opt_disable_updater = false; +bool opt_disable_missing_files_check = false; +string opt_starting_collection; +string opt_starting_profile; +string opt_starting_scene; + +bool restart = false; +bool restart_safe = false; +QStringList arguments; + +QPointer obsLogViewer; + +#ifndef _WIN32 +int OBSApp::sigintFd[2]; +#endif + +// GPU hint exports for AMD/NVIDIA laptops +#ifdef _MSC_VER +extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; +extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +#endif + +QObject *CreateShortcutFilter() +{ + return new OBSEventFilter([](QObject *obj, QEvent *event) { + auto mouse_event = [](QMouseEvent &event) { + if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) + return true; + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event.type() == QEvent::MouseButtonPress; + + switch (event.button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return false; + + case Qt::MiddleButton: + hotkey.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + hotkey.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4); + MAP_BUTTON(2, 5); + MAP_BUTTON(3, 6); + MAP_BUTTON(4, 7); + MAP_BUTTON(5, 8); + MAP_BUTTON(6, 9); + MAP_BUTTON(7, 10); + MAP_BUTTON(8, 11); + MAP_BUTTON(9, 12); + MAP_BUTTON(10, 13); + MAP_BUTTON(11, 14); + MAP_BUTTON(12, 15); + MAP_BUTTON(13, 16); + MAP_BUTTON(14, 17); + MAP_BUTTON(15, 18); + MAP_BUTTON(16, 19); + MAP_BUTTON(17, 20); + MAP_BUTTON(18, 21); + MAP_BUTTON(19, 22); + MAP_BUTTON(20, 23); + MAP_BUTTON(21, 24); + MAP_BUTTON(22, 25); + MAP_BUTTON(23, 26); + MAP_BUTTON(24, 27); +#undef MAP_BUTTON + } + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + auto key_event = [&](QKeyEvent *event) { + int key = event->key(); + bool enabledInFocus = App()->HotkeysEnabledInFocus(); + + if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) + return true; + + QDialog *dialog = qobject_cast(obj); + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event->type() == QEvent::KeyPress; + + switch (key) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + hotkey.key = obs_key_from_virtual_key(57); + pressed = true; + break; +#endif + + case Qt::Key_Enter: + case Qt::Key_Escape: + case Qt::Key_Return: + if (dialog && pressed) + return false; + if (!enabledInFocus) + return true; + /* Falls through. */ + default: + hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + if (event->isAutoRepeat()) + return true; + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + return mouse_event(*static_cast(event)); + + /*case QEvent::MouseButtonDblClick: + case QEvent::Wheel:*/ + case QEvent::KeyPress: + case QEvent::KeyRelease: + return key_event(static_cast(event)); + + default: + return false; + } + }); +} + +string CurrentTimeString() +{ + using namespace std::chrono; + + struct tm tstruct; + char buf[80]; + + auto tp = system_clock::now(); + auto now = system_clock::to_time_t(tp); + tstruct = *localtime(&now); + + size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); + if (ratio_less::value && written && (sizeof(buf) - written) > 5) { + auto tp_secs = time_point_cast(tp); + auto millis = duration_cast(tp - tp_secs).count(); + + snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); + } + + return buf; +} + +string CurrentDateTimeString() +{ + time_t now = time(0); + struct tm tstruct; + char buf[80]; + tstruct = *localtime(&now); + strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); + return buf; +} + +static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) +{ + static mutex logfile_mutex; + string msg; + msg += timeString; + msg += str; + + logfile_mutex.lock(); + logFile << msg << endl; + logfile_mutex.unlock(); + + if (!!obsLogViewer) + QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), + Q_ARG(QString, QString(msg.c_str()))); +} + +static inline void LogStringChunk(fstream &logFile, char *str, int log_level) +{ + char *nextLine = str; + string timeString = CurrentTimeString(); + timeString += ": "; + + while (*nextLine) { + char *nextLine = strchr(str, '\n'); + if (!nextLine) + break; + + if (nextLine != str && nextLine[-1] == '\r') { + nextLine[-1] = 0; + } else { + nextLine[0] = 0; + } + + LogString(logFile, timeString.c_str(), str, log_level); + nextLine++; + str = nextLine; + } + + LogString(logFile, timeString.c_str(), str, log_level); +} + +#define MAX_REPEATED_LINES 30 +#define MAX_CHAR_VARIATION (255 * 3) + +static inline int sum_chars(const char *str) +{ + int val = 0; + for (; *str != 0; str++) + val += *str; + + return val; +} + +static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) +{ + static mutex log_mutex; + static const char *last_msg_ptr = nullptr; + static int last_char_sum = 0; + static int rep_count = 0; + + int new_sum = sum_chars(output_str); + + lock_guard guard(log_mutex); + + if (unfiltered_log) { + return false; + } + + if (last_msg_ptr == msg) { + int diff = std::abs(new_sum - last_char_sum); + if (diff < MAX_CHAR_VARIATION) { + return (rep_count++ >= MAX_REPEATED_LINES); + } + } + + if (rep_count > MAX_REPEATED_LINES) { + logFile << CurrentTimeString() << ": Last log entry repeated for " + << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; + } + + last_msg_ptr = msg; + last_char_sum = new_sum; + rep_count = 0; + + return false; +} + +static void do_log(int log_level, const char *msg, va_list args, void *param) +{ + fstream &logFile = *static_cast(param); + char str[8192]; + +#ifndef _WIN32 + va_list args2; + va_copy(args2, args); +#endif + + vsnprintf(str, sizeof(str), msg, args); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (wNum > 1) { + static wstring wide_buf; + static mutex wide_mutex; + + lock_guard lock(wide_mutex); + wide_buf.reserve(wNum + 1); + wide_buf.resize(wNum - 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); + wide_buf.push_back('\n'); + + OutputDebugStringW(wide_buf.c_str()); + } + } +#endif + +#if !defined(_WIN32) && defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + + if (log_level <= LOG_INFO || log_verbose) { +#if !defined(_WIN32) && !defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + if (!too_many_repeated_entries(logFile, msg, str)) + LogStringChunk(logFile, str, log_level); + } + +#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) + if (log_level <= LOG_ERROR && IsDebuggerPresent()) + __debugbreak(); +#endif + +#ifndef _WIN32 + va_end(args2); +#endif +} + +#define DEFAULT_LANG "en-US" + +bool OBSApp::InitGlobalConfigDefaults() +{ + config_set_default_uint(appConfig, "General", "MaxLogs", 10); + config_set_default_int(appConfig, "General", "InfoIncrement", -1); + config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); + config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); + +#if _WIN32 + config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); +#else + config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); +#endif + +#ifdef _WIN32 + config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); +#endif + +#ifdef __APPLE__ + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); + config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); + config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); +#endif + + return true; +} + +bool OBSApp::InitGlobalLocationDefaults() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(NULL, "Unable to get global configuration path."); + return false; + } + + config_set_default_string(appConfig, "Locations", "Configuration", path); + config_set_default_string(appConfig, "Locations", "SceneCollections", path); + config_set_default_string(appConfig, "Locations", "Profiles", path); + + return true; +} + +void OBSApp::InitUserConfigDefaults() +{ + config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); + + config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); + + config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); + config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); + config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); + config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); + config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); + config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); + + config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); + + config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); +} + +static bool do_mkdir(const char *path) +{ + if (os_mkdirs(path) == MKDIR_ERROR) { + OBSErrorBox(NULL, "Failed to create directory %s", path); + return false; + } + + return true; +} + +static bool MakeUserDirs() +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) + return false; + if (!do_mkdir(path)) + return false; + +#ifdef _WIN32 + if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + +#ifdef WHATSNEW_ENABLED + if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + return true; +} + +constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; +constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; + +static bool MakeUserProfileDirs() +{ + const std::filesystem::path userProfilePath = + App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); + const std::filesystem::path userScenesPath = + App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); + + if (!std::filesystem::exists(userProfilePath)) { + try { + std::filesystem::create_directories(userProfilePath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create user profile directory '%s'\n%s", + userProfilePath.u8string().c_str(), error.what()); + return false; + } + } + + if (!std::filesystem::exists(userScenesPath)) { + try { + std::filesystem::create_directories(userScenesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create user scene collection directory '%s'\n%s", + userScenesPath.u8string().c_str(), error.what()); + return false; + } + } + + return true; +} + +bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) +{ + if (!layout) + return false; + + if (astrcmpi(layout, "horizontaltop") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "horizontalbottom") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalleft") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalright") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + return true; + } + + return false; +} + +bool OBSApp::InitGlobalConfig() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); + if (len <= 0) { + return false; + } + + int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); + if (errorcode != CONFIG_SUCCESS) { + OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); + return false; + } + + uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); + + if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { + bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); + + if (!migratedUserSettings) { + bool migrated = MigrateGlobalSettings(); + + config_set_bool(appConfig, "General", "Pre31Migrated", migrated); + config_save_safe(appConfig, "tmp", nullptr); + } + } + + InitGlobalConfigDefaults(); + InitGlobalLocationDefaults(); + + if (IsPortableMode()) { + userConfigLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); + } else { + userConfigLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); + } + + bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); + + return userConfigResult; +} + +bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) +{ + const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; + + int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); + + if (errorCode != CONFIG_SUCCESS) { + OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); + return false; + } + + MigrateLegacySettings(lastVersion); + InitUserConfigDefaults(); + + return true; +} + +void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) +{ + bool hasChanges = false; + + const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); + const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); + const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); + const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); + const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); + + const map defaultsMap{ + {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; + + for (auto &[version, configKey] : defaultsMap) { + if (!config_has_user_value(userConfig, "General", configKey.c_str())) { + bool useOldDefaults = lastVersion && lastVersion < version; + config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); + + hasChanges = true; + } + } + + if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { + const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); + + bool layoutUpdated = UpdatePre22MultiviewLayout(layout); + + hasChanges = hasChanges | layoutUpdated; + } + + if (lastVersion && lastVersion < v24) { + bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); + + if (disableHotkeysInFocus) { + config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); + } + + hasChanges = true; + } + + if (hasChanges) { + userConfig.SaveSafe("tmp"); + } +} + +static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; +static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; + +bool OBSApp::MigrateGlobalSettings() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(nullptr, "Unable to get global configuration path."); + return false; + } + + std::string legacyConfigFileString; + legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); + legacyConfigFileString.append(path).append(OBSGlobalIniPath); + + const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); + + std::string configFileString; + configFileString.reserve(strlen(path) + OBSUserIniPath.size()); + configFileString.append(path).append(OBSUserIniPath); + + const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); + + if (std::filesystem::exists(userConfigFile)) { + OBSErrorBox(nullptr, + "Unable to migrate global configuration - user configuration file already exists."); + return false; + } + + try { + std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); + } catch (const std::filesystem::filesystem_error &) { + OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); + return false; + } + + return true; +} + +bool OBSApp::InitLocale() +{ + ProfileScope("OBSApp::InitLocale"); + + const char *lang = config_get_string(userConfig, "General", "Language"); + bool userLocale = config_has_user_value(userConfig, "General", "Language"); + if (!userLocale || !lang || lang[0] == '\0') + lang = DEFAULT_LANG; + + locale = lang; + + // set basic default application locale + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + string englishPath; + if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { + OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); + return false; + } + + textLookup = text_lookup_create(englishPath.c_str()); + if (!textLookup) { + OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); + return false; + } + + bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; + + if (userLocale && defaultLang) + return true; + + if (!userLocale && defaultLang) { + for (auto &locale_ : GetPreferredLocales()) { + if (locale_ == lang) + return true; + + stringstream file; + file << "locale/" << locale_ << ".ini"; + + string path; + if (!GetDataFilePath(file.str().c_str(), path)) + continue; + + if (!text_lookup_add(textLookup, path.c_str())) + continue; + + blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); + locale = locale_; + + // set application default locale to the new choosen one + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + return true; + } + + return true; + } + + stringstream file; + file << "locale/" << lang << ".ini"; + + string path; + if (GetDataFilePath(file.str().c_str(), path)) { + if (!text_lookup_add(textLookup, path.c_str())) + blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); + } else { + blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); + } + + return true; +} + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) +{ + JsonBranches branches; + + try { + nlohmann::json json = nlohmann::json::parse(jsonString); + branches = json.get(); + } catch (nlohmann::json::exception &e) { + error = e.what(); + return; + } + + for (const JsonBranch &json_branch : branches) { +#ifdef _WIN32 + if (!json_branch.windows) + continue; +#elif defined(__APPLE__) + if (!json_branch.macos) + continue; +#endif + + UpdateBranch branch = { + QString::fromStdString(json_branch.name), + QString::fromStdString(json_branch.display_name), + QString::fromStdString(json_branch.description), + json_branch.enabled, + json_branch.visible, + }; + out.push_back(branch); + } +} + +bool LoadBranchesFile(vector &out) +{ + string error; + string branchesText; + + BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); + + QFile branchesFile(branchesFilePath.Get()); + if (!branchesFile.open(QIODevice::ReadOnly)) { + error = "Opening file failed."; + goto fail; + } + + branchesText = branchesFile.readAll(); + if (branchesText.empty()) { + error = "File empty."; + goto fail; + } + + ParseBranchesJson(branchesText, out, error); + if (error.empty()) + return !out.empty(); + +fail: + blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); + return false; +} +#endif + +void OBSApp::SetBranchData(const string &data) +{ +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + string error; + vector result; + + ParseBranchesJson(data, result, error); + + if (!error.empty()) { + blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); + return; + } + + if (!result.empty()) + updateBranches = result; + + branches_loaded = true; +#else + UNUSED_PARAMETER(data); +#endif +} + +std::vector OBSApp::GetBranches() +{ + vector out; + /* Always ensure the default branch exists */ + out.push_back(UpdateBranch{"stable", "", "", true, true}); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + if (!branches_loaded) { + vector result; + if (LoadBranchesFile(result)) + updateBranches = result; + + branches_loaded = true; + } +#endif + + /* Copy additional branches to result (if any) */ + if (!updateBranches.empty()) + out.insert(out.end(), updateBranches.begin(), updateBranches.end()); + + return out; +} + +OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) + : QApplication(argc, argv), + profilerNameStore(store) +{ + /* fix float handling */ +#if defined(Q_OS_UNIX) + if (!setlocale(LC_NUMERIC, "C")) + blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); +#endif + +#ifndef _WIN32 + /* Handle SIGINT properly */ + 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); +#endif + + sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); + +#ifndef __APPLE__ + setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); +#endif + + setDesktopFileName("com.obsproject.Studio"); +} + +OBSApp::~OBSApp() +{ +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(false); +#else + delete snInt; + close(sigintFd[0]); + close(sigintFd[1]); +#endif + +#ifdef __APPLE__ + bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); + bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); + if (vsyncDisabled && resetVSync) + EnableOSXVSync(true); +#endif + + os_inhibit_sleep_set_active(sleepInhibitor, false); + os_inhibit_sleep_destroy(sleepInhibitor); + + if (libobs_initialized) + obs_shutdown(); +} + +static void move_basic_to_profiles(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path profilesPath = + App()->userProfilesLocation / std::filesystem::u8path("obs-studio/basic/profiles"); + + if (std::filesystem::exists(profilesPath)) { + return; + } + + try { + std::filesystem::create_directories(profilesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create profiles directory for migration from basic profile\n%s", + error.what()); + return; + } + + const std::filesystem::path newProfilePath = profilesPath / std::filesystem::u8path(Str("Untitled")); + + for (auto &entry : std::filesystem::directory_iterator(basicPath)) { + if (entry.is_directory()) { + continue; + } + + if (entry.path().filename().u8string() == "scenes.json") { + continue; + } + + if (!std::filesystem::exists(newProfilePath)) { + try { + std::filesystem::create_directory(newProfilePath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to create profile directory for 'Untitled'\n%s", error.what()); + return; + } + } + + const filesystem::path destinationFile = newProfilePath / entry.path().filename(); + + const auto copyOptions = std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(entry.path(), destinationFile, copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s", + entry.path().filename().u8string().c_str(), error.what()); + + return; + } + } +} + +static void move_basic_to_scene_collections(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path sceneCollectionPath = + App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); + + if (std::filesystem::exists(sceneCollectionPath)) { + return; + } + + try { + std::filesystem::create_directories(sceneCollectionPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create scene collection directory for migration from basic scene collection\n%s", + error.what()); + return; + } + + const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); + const std::filesystem::path destinationFile = + (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); + + try { + std::filesystem::rename(sourceFile, destinationFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); + return; + } +} + +void OBSApp::AppInit() +{ + ProfileScope("OBSApp::AppInit"); + + if (!MakeUserDirs()) + throw "Failed to create required user directories"; + if (!InitGlobalConfig()) + throw "Failed to initialize global config"; + if (!InitLocale()) + throw "Failed to load locale"; + if (!InitTheme()) + throw "Failed to load theme"; + + config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); + + if (!config_has_user_value(userConfig, "Basic", "Profile")) { + config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + } + + if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { + config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + } + +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(true); +#endif + +#ifdef __APPLE__ + if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) + EnableOSXVSync(false); +#endif + + UpdateHotkeyFocusSetting(false); + + move_basic_to_profiles(); + move_basic_to_scene_collections(); + + if (!MakeUserProfileDirs()) + throw "Failed to create profile directories"; +} + +const char *OBSApp::GetRenderModule() const +{ + const char *renderer = config_get_string(appConfig, "Video", "Renderer"); + + return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; +} + +static bool StartupOBS(const char *locale, profiler_name_store_t *store) +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + + return obs_startup(locale, path, store); +} + +inline void OBSApp::ResetHotkeyState(bool inFocus) +{ + obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); +} + +void OBSApp::UpdateHotkeyFocusSetting(bool resetState) +{ + enableHotkeysInFocus = true; + enableHotkeysOutOfFocus = true; + + const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); + + if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { + enableHotkeysInFocus = false; + } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { + enableHotkeysOutOfFocus = false; + } + + if (resetState) + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +void OBSApp::DisableHotkeys() +{ + enableHotkeysInFocus = false; + enableHotkeysOutOfFocus = false; + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +Q_DECLARE_METATYPE(VoidFunc) + +void OBSApp::Exec(VoidFunc func) +{ + func(); +} + +static void ui_task_handler(obs_task_t task, void *param, bool wait) +{ + auto doTask = [=]() { + /* to get clang-format to behave */ + task(param); + }; + QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); +} + +bool OBSApp::OBSInit() +{ + ProfileScope("OBSApp::OBSInit"); + + qRegisterMetaType("VoidFunc"); + +#if !defined(_WIN32) && !defined(__APPLE__) + if (QApplication::platformName() == "xcb") { + obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); + blog(LOG_INFO, "Using EGL/X11"); + } + +#ifdef ENABLE_WAYLAND + if (QApplication::platformName().contains("wayland")) { + obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + blog(LOG_INFO, "Platform: Wayland"); + } +#endif + + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); +#endif + +#ifdef __APPLE__ + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#endif + + if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) + return false; + + libobs_initialized = true; + + obs_set_ui_task_handler(ui_task_handler); + +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); + obs_apply_private_data(settings); + + blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); + + blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); +#endif +#ifdef _WIN32 + bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); + blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); +#endif + + blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); + blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); + + if (safe_mode) { + blog(LOG_WARNING, "Safe Mode enabled."); + } else if (disable_3p_plugins) { + blog(LOG_WARNING, "Third-party plugins disabled."); + } + + setQuitOnLastWindowClosed(false); + + mainWindow = new OBSBasic(); + + mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); + connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); + + mainWindow->OBSInit(); + + connect(this, &QGuiApplication::applicationStateChanged, + [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); + ResetHotkeyState(applicationState() == Qt::ApplicationActive); + return true; +} + +string OBSApp::GetVersionString(bool platform) const +{ + stringstream ver; + +#ifdef HAVE_OBSCONFIG_H + ver << obs_get_version_string(); +#else + ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; + +#endif + + if (platform) { + ver << " ("; +#ifdef _WIN32 + if (sizeof(void *) == 8) + ver << "64-bit, "; + else + ver << "32-bit, "; + + ver << "windows)"; +#elif __APPLE__ + ver << "mac)"; +#elif __OpenBSD__ + ver << "openbsd)"; +#elif __FreeBSD__ + ver << "freebsd)"; +#else /* assume linux for the time being */ + ver << "linux)"; +#endif + } + + return ver.str(); +} + +bool OBSApp::IsPortableMode() +{ + return portable_mode; +} + +bool OBSApp::IsUpdaterDisabled() +{ + return opt_disable_updater; +} + +bool OBSApp::IsMissingFilesCheckDisabled() +{ + return opt_disable_missing_files_check; +} + +#ifdef __APPLE__ +#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" +#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" +#elif _WIN32 +#define INPUT_AUDIO_SOURCE "wasapi_input_capture" +#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" +#else +#define INPUT_AUDIO_SOURCE "pulse_input_capture" +#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" +#endif + +const char *OBSApp::InputAudioSource() const +{ + return INPUT_AUDIO_SOURCE; +} + +const char *OBSApp::OutputAudioSource() const +{ + return OUTPUT_AUDIO_SOURCE; +} + +const char *OBSApp::GetLastLog() const +{ + return lastLogFile.c_str(); +} + +const char *OBSApp::GetCurrentLog() const +{ + return currentLogFile.c_str(); +} + +const char *OBSApp::GetLastCrashLog() const +{ + return lastCrashLogFile.c_str(); +} + +bool OBSApp::TranslateString(const char *lookupVal, const char **out) const +{ + for (obs_frontend_translate_ui_cb cb : translatorHooks) { + if (cb(lookupVal, out)) + return true; + } + + return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); +} + +// Global handler to receive all QEvent::Show events so we can apply +// display affinity on any newly created windows and dialogs without +// caring where they are coming from (e.g. plugins). +bool OBSApp::notify(QObject *receiver, QEvent *e) +{ + QWidget *w; + QWindow *window; + int windowType; + + if (!receiver->isWidgetType()) + goto skip; + + if (e->type() != QEvent::Show) + goto skip; + + w = qobject_cast(receiver); + + if (!w->isWindow()) + goto skip; + + window = w->windowHandle(); + if (!window) + goto skip; + + windowType = window->flags() & Qt::WindowType::WindowType_Mask; + + if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || + windowType == Qt::WindowType::Tool) { + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->SetDisplayAffinity(window); + } + +skip: + return QApplication::notify(receiver, e); +} + +QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const +{ + const char *out = nullptr; + QString str(sourceText); + str.replace(" ", ""); + if (!App()->TranslateString(QT_TO_UTF8(str), &out)) + return QString(sourceText); + + return QT_UTF8(out); +} + +static bool get_token(lexer *lex, string &str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + str.assign(token.text.array, token.text.len); + return true; +} + +static bool expect_token(lexer *lex, const char *str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + return strref_cmp(&token.text, str) == 0; +} + +static uint64_t convert_log_name(bool has_prefix, const char *name) +{ + BaseLexer lex; + string year, month, day, hour, minute, second; + + lexer_start(lex, name); + + if (has_prefix) { + string temp; + if (!get_token(lex, temp, BASETOKEN_ALPHA)) + return 0; + } + + if (!get_token(lex, year, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, month, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, day, BASETOKEN_DIGIT)) + return 0; + if (!get_token(lex, hour, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, minute, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, second, BASETOKEN_DIGIT)) + return 0; + + stringstream timestring; + timestring << year << month << day << hour << minute << second; + return std::stoull(timestring.str()); +} + +/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ +/* TODO: Remove after version 32.0. */ +#if defined(__FreeBSD__) +static void move_to_xdg(void) +{ + char old_path[512]; + char new_path[512]; + char *home = getenv("HOME"); + if (!home) + return; + + if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) + return; + + /* make base xdg path if it doesn't already exist */ + if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) + return; + if (os_mkdirs(new_path) == MKDIR_ERROR) + return; + + if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) + return; + + if (os_file_exists(old_path) && !os_file_exists(new_path)) { + rename(old_path, new_path); + } +} +#endif + +static void delete_oldest_file(bool has_prefix, const char *location) +{ + BPtr logDir(GetAppConfigPathPtr(location)); + string oldestLog; + uint64_t oldest_ts = (uint64_t)-1; + struct os_dirent *entry; + + unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); + + os_dir_t *dir = os_opendir(logDir); + if (dir) { + unsigned int count = 0; + + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts) { + if (ts < oldest_ts) { + oldestLog = entry->d_name; + oldest_ts = ts; + } + + count++; + } + } + + os_closedir(dir); + + if (count > maxLogs) { + stringstream delPath; + + delPath << logDir << "/" << oldestLog; + os_unlink(delPath.str().c_str()); + } + } +} + +static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) +{ + BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); + struct os_dirent *entry; + os_dir_t *dir = os_opendir(logDir); + uint64_t highest_ts = 0; + + if (dir) { + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts > highest_ts) { + last = entry->d_name; + highest_ts = ts; + } + } + + os_closedir(dir); + } +} + +string GenerateTimeDateFilename(const char *extension, bool noSpace) +{ + time_t now = time(0); + char file[256] = {}; + struct tm *cur_time; + + cur_time = localtime(&now); + snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, + cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, + extension); + + return string(file); +} + +string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) +{ + BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); + return string(filename); +} + +static void FindBestFilename(string &strPath, bool noSpace) +{ + int num = 2; + + if (!os_file_exists(strPath.c_str())) + return; + + const char *ext = strrchr(strPath.c_str(), '.'); + if (!ext) + return; + + int extStart = int(ext - strPath.c_str()); + for (;;) { + string testPath = strPath; + string numStr; + + numStr = noSpace ? "_" : " ("; + numStr += to_string(num++); + if (!noSpace) + numStr += ")"; + + testPath.insert(extStart, numStr); + + if (!os_file_exists(testPath.c_str())) { + strPath = testPath; + break; + } + } +} + +static void ensure_directory_exists(string &path) +{ + replace(path.begin(), path.end(), '\\', '/'); + + size_t last = path.rfind('/'); + if (last == string::npos) + return; + + string directory = path.substr(0, last); + os_mkdirs(directory.c_str()); +} + +static void remove_reserved_file_characters(string &s) +{ + replace(s.begin(), s.end(), '\\', '/'); + replace(s.begin(), s.end(), '*', '_'); + replace(s.begin(), s.end(), '?', '_'); + replace(s.begin(), s.end(), '"', '_'); + replace(s.begin(), s.end(), '|', '_'); + replace(s.begin(), s.end(), ':', '_'); + replace(s.begin(), s.end(), '>', '_'); + replace(s.begin(), s.end(), '<', '_'); +} + +string GetFormatString(const char *format, const char *prefix, const char *suffix) +{ + string f; + + f = format; + + if (prefix && *prefix) { + string str_prefix = prefix; + + if (str_prefix.back() != ' ') + str_prefix += " "; + + size_t insert_pos = 0; + size_t tmp; + + tmp = f.find_last_of('/'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + tmp = f.find_last_of('\\'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + f.insert(insert_pos, str_prefix); + } + + if (suffix && *suffix) { + if (*suffix != ' ') + f += " "; + f += suffix; + } + + remove_reserved_file_characters(f); + + return f; +} + +string GetFormatExt(const char *container) +{ + string ext = container; + if (ext == "fragmented_mp4") + ext = "mp4"; + if (ext == "hybrid_mp4") + ext = "mp4"; + else if (ext == "fragmented_mov") + ext = "mov"; + else if (ext == "hls") + ext = "m3u8"; + else if (ext == "mpegts") + ext = "ts"; + + return ext; +} + +string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; + + if (!dir) { + if (main->isVisible()) + OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); + return ""; + } + + os_closedir(dir); + + string strPath; + strPath += path; + + char lastChar = strPath.back(); + if (lastChar != '/' && lastChar != '\\') + strPath += "/"; + + string ext = GetFormatExt(container); + strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); + ensure_directory_exists(strPath); + if (!overwrite) + FindBestFilename(strPath, noSpace); + + return strPath; +} + +vector> GetLocaleNames() +{ + string path; + if (!GetDataFilePath("locale.ini", path)) + throw "Could not find locale.ini path"; + + ConfigFile ini; + if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) + throw "Could not open locale.ini"; + + size_t sections = config_num_sections(ini); + + vector> names; + names.reserve(sections); + for (size_t i = 0; i < sections; i++) { + const char *tag = config_get_section(ini, i); + const char *name = config_get_string(ini, tag, "Name"); + names.emplace_back(tag, name); + } + + return names; +} + +static void create_log_file(fstream &logFile) +{ + stringstream dst; + + get_last_log(false, "obs-studio/logs", lastLogFile); +#ifdef _WIN32 + get_last_log(true, "obs-studio/crashes", lastCrashLogFile); +#endif + + currentLogFile = GenerateTimeDateFilename("txt"); + dst << "obs-studio/logs/" << currentLogFile.c_str(); + + BPtr path(GetAppConfigPathPtr(dst.str().c_str())); + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); +#else + logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); +#endif + + if (logFile.is_open()) { + delete_oldest_file(false, "obs-studio/logs"); + base_set_log_handler(do_log, &logFile); + } else { + blog(LOG_ERROR, "Failed to open log file"); + } +} + +static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { + profiler_name_store_free(store); +}; + +using ProfilerNameStore = std::unique_ptr; + +ProfilerNameStore CreateNameStore() +{ + return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; +} + +static auto SnapshotRelease = [](profiler_snapshot_t *snap) { + profile_snapshot_free(snap); +}; + +using ProfilerSnapshot = std::unique_ptr; + +ProfilerSnapshot GetSnapshot() +{ + return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; +} + +static void SaveProfilerData(const ProfilerSnapshot &snap) +{ + if (currentLogFile.empty()) + return; + + auto pos = currentLogFile.rfind('.'); + if (pos == currentLogFile.npos) + return; + +#define LITERAL_SIZE(x) x, (sizeof(x) - 1) + ostringstream dst; + dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); + dst.write(currentLogFile.c_str(), pos); + dst.write(LITERAL_SIZE(".csv.gz")); +#undef LITERAL_SIZE + + BPtr path = GetAppConfigPathPtr(dst.str().c_str()); + if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) + blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); +} + +static auto ProfilerFree = [](void *) { + profiler_stop(); + + auto snap = GetSnapshot(); + + profiler_print(snap.get()); + profiler_print_time_between_calls(snap.get()); + + SaveProfilerData(snap); + + profiler_free(); +}; + +QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) + return new VolumeAccessibleInterface(static_cast(object)); + + return nullptr; +} + +static const char *run_program_init = "run_program_init"; +static int run_program(fstream &logFile, int argc, char *argv[]) +{ + int ret = -1; + + auto profilerNameStore = CreateNameStore(); + + std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); + + profiler_start(); + profile_register_root(run_program_init, 0); + + ScopeProfiler prof{run_program_init}; + +#ifdef _WIN32 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + + QCoreApplication::addLibraryPath("."); + +#if __APPLE__ + InstallNSApplicationSubclass(); + InstallNSThreadLocks(); + + if (!isInBundle()) { + blog(LOG_ERROR, + "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); + return ret; + } +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) + /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and + * crashes loading saved geometry. Just turn off this theme and let users complain OBS + * looks ugly instead of crashing. */ + const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); + if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) + unsetenv("QT_QPA_PLATFORMTHEME"); +#endif + + /* NOTE: This disables an optimisation in Qt that attempts to determine if + * any "siblings" intersect with a widget when determining the approximate + * visible/unobscured area. However, by Qt's own admission this is slow + * and in the case of OBS it significantly slows down lists with many + * elements (e.g. Hotkeys) and it is actually faster to disable it. */ + qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); + + OBSApp program(argc, argv, profilerNameStore.get()); + try { + QAccessible::installFactory(accessibleFactory); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + + bool created_log = false; + + program.AppInit(); + delete_oldest_file(false, "obs-studio/profiler_data"); + + OBSTranslator translator; + program.installTranslator(&translator); + + /* --------------------------------------- */ + /* check and warn if already running */ + + bool cancel_launch = false; + bool already_running = false; + +#ifdef _WIN32 + RunOnceMutex rom = +#endif + CheckIfAlreadyRunning(already_running); + + if (!already_running) { + goto run; + } + + if (!multi) { + QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), + QTStr("AlreadyRunning.Text")); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); + QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); + mb.setDefaultButton(cancelButton); + + mb.exec(); + cancel_launch = mb.clickedButton() == cancelButton; + } + + if (cancel_launch) + return 0; + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { + blog(LOG_INFO, "User enabled --multi flag and is now " + "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); + /* Clear unclean_shutdown flag as multiple instances + * running from the same config will lead to a + * false-positive detection.*/ + unclean_shutdown = false; + } + + /* --------------------------------------- */ + run: + +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) + // Mounted by termina during chromeOS linux container startup + // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh + os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); + if (crosDir) { + QMessageBox::StandardButtons buttons(QMessageBox::Ok); + QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, + nullptr); + + mb.exec(); + return 0; + } +#endif + + if (!created_log) + create_log_file(logFile); + + if (unclean_shutdown) { + blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); + } + + if (unclean_shutdown && !safe_mode) { + QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); + QPushButton *launchSafeButton = + mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); + QPushButton *launchNormalButton = + mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); + mb.setDefaultButton(launchNormalButton); + mb.exec(); + + safe_mode = mb.clickedButton() == launchSafeButton; + if (safe_mode) { + blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); + } else { + blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); + } + } + + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { + switch (type) { +#ifdef _DEBUG + case QtDebugMsg: + blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); + break; + case QtInfoMsg: + blog(LOG_INFO, "%s", QT_TO_UTF8(message)); + break; +#else + case QtDebugMsg: + case QtInfoMsg: + break; +#endif + case QtWarningMsg: + blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); + break; + case QtCriticalMsg: + case QtFatalMsg: + blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); + break; + } + }); + +#ifdef __APPLE__ + MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); + MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); + MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); + MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); + + int permissionsDialogLastShown = + config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); + if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { + OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, + accessibility_permission); + check.exec(); + } +#endif + +#ifdef _WIN32 + if (IsRunningOnWine()) { + QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); + mb.setTextFormat(Qt::RichText); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); + QPushButton *closeButton = mb.addButton(QMessageBox::Close); + mb.setDefaultButton(closeButton); + + mb.exec(); + if (mb.clickedButton() == closeButton) + return 0; + } +#endif + + if (argc > 1) { + stringstream stor; + stor << argv[1]; + for (int i = 2; i < argc; ++i) { + stor << " " << argv[i]; + } + blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); + } + + if (!program.OBSInit()) + return 0; + + prof.Stop(); + + ret = program.exec(); + + } catch (const char *error) { + blog(LOG_ERROR, "%s", error); + OBSErrorBox(nullptr, "%s", error); + } + + if (restart || restart_safe) { + arguments = qApp->arguments(); + + if (restart_safe) { + arguments.append("--safe-mode"); + } else { + arguments.removeAll("--safe-mode"); + } + } + + return ret; +} + +#define MAX_CRASH_REPORT_SIZE (150 * 1024) + +#ifdef _WIN32 + +#define CRASH_MESSAGE \ + "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ + "to the clipboard? The crash log will still be saved to:\n\n%s" + +static void main_crash_handler(const char *format, va_list args, void * /* param */) +{ + char *text = new char[MAX_CRASH_REPORT_SIZE]; + + vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); + text[MAX_CRASH_REPORT_SIZE - 1] = 0; + + string crashFilePath = "obs-studio/crashes"; + + delete_oldest_file(true, crashFilePath.c_str()); + + string name = crashFilePath + "/"; + name += "Crash " + GenerateTimeDateFilename("txt"); + + BPtr path(GetAppConfigPathPtr(name.c_str())); + + fstream file; + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#else + file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#endif + file << text; + file.close(); + + string pathString(path.Get()); + +#ifdef _WIN32 + std::replace(pathString.begin(), pathString.end(), '/', '\\'); +#endif + + string absolutePath = canonical(filesystem::path(pathString)).u8string(); + + size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); + + unique_ptr message_buffer(new char[size + 1]); + + snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); + + string finalMessage = string(message_buffer.get(), message_buffer.get() + size); + + int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); + + if (ret == IDYES) { + size_t len = strlen(text); + + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(mem), text, len); + GlobalUnlock(mem); + + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + + exit(-1); +} + +static void load_debug_privilege(void) +{ + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { + return; + } + + if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); + } + + if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + blog(LOG_INFO, "Could not set privilege to " + "increase GPU priority"); + } + } + + CloseHandle(token); +} +#endif + +#ifdef __APPLE__ +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#define CONFIG_PATH BASE_PATH "/config" + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + +int GetAppConfigPath(char *path, size_t size, const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + if (name && *name) { + return snprintf(path, size, CONFIG_PATH "/%s", name); + } else { + return snprintf(path, size, CONFIG_PATH); + } + } else { + return os_get_config_path(path, size, name); + } +#else + return os_get_config_path(path, size, name); +#endif +} + +char *GetAppConfigPathPtr(const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + char path[512]; + + if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { + return bstrdup(path); + } else { + return NULL; + } + } else { + return os_get_config_path_ptr(name); + } +#else + return os_get_config_path_ptr(name); +#endif +} + +int GetProgramDataPath(char *path, size_t size, const char *name) +{ + return os_get_program_data_path(path, size, name); +} + +char *GetProgramDataPathPtr(const char *name) +{ + return os_get_program_data_path_ptr(name); +} + +bool GetFileSafeName(const char *name, std::string &file) +{ + size_t base_len = strlen(name); + size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); + std::wstring wfile; + + if (!len) + return false; + + wfile.resize(len); + os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); + + for (size_t i = wfile.size(); i > 0; i--) { + size_t im1 = i - 1; + + if (iswspace(wfile[im1])) { + wfile[im1] = '_'; + } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { + wfile.erase(im1, 1); + } + } + + if (wfile.size() == 0) + wfile = L"characters_only"; + + len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); + if (!len) + return false; + + file.resize(len); + os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); + return true; +} + +bool GetClosestUnusedFileName(std::string &path, const char *extension) +{ + size_t len = path.size(); + if (extension) { + path += "."; + path += extension; + } + + if (!os_file_exists(path.c_str())) + return true; + + int index = 1; + + do { + path.resize(len); + path += std::to_string(++index); + if (extension) { + path += "."; + path += extension; + } + } while (os_file_exists(path.c_str())); + + return true; +} + +bool WindowPositionValid(QRect rect) +{ + for (QScreen *screen : QGuiApplication::screens()) { + if (screen->availableGeometry().intersects(rect)) + return true; + } + return false; +} + +static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) +{ + return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); +} + +static void check_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + /* Safe Mode detection is disabled in Debug builds to keep developers + * somewhat sane. */ + return; +#else + if (disable_shutdown_check) + return; + + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + if (os_file_exists(sentinelPath)) { + unclean_shutdown = true; + return; + } + + os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); +#endif +} + +static void delete_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + return; +#else + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + os_unlink(sentinelPath); +#endif +} + +#ifndef _WIN32 +void OBSApp::SigIntSignalHandler(int s) +{ + /* Handles SIGINT and writes to a socket. Qt will read + * from the socket in the main thread event loop and trigger + * a call to the ProcessSigInt slot, where we can safely run + * shutdown code without signal safety issues. */ + UNUSED_PARAMETER(s); + + char a = 1; + send(sigintFd[0], &a, sizeof(a), 0); +} +#endif + +void OBSApp::ProcessSigInt(void) +{ + /* This looks weird, but we can't ifdef a Qt slot function so + * the SIGINT handler simply does nothing on Windows. */ +#ifndef _WIN32 + char tmp; + recv(sigintFd[1], &tmp, sizeof(tmp), 0); + + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->close(); +#endif +} + +#ifdef _WIN32 +void OBSApp::commitData(QSessionManager &manager) +{ + if (auto main = App()->GetMainWindow()) { + QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); + manager.cancel(); + } +} +#endif + +#ifdef _WIN32 +static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; +static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " + "Redistributables.\n\nYou will now be directed to the download page."; +static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; + +static bool vc_runtime_outdated() +{ + win_version_info ver; + if (!get_dll_ver(L"msvcp140.dll", &ver)) + return true; + /* Major is always 14 (hence 140.dll), so we only care about minor. */ + if (ver.minor >= 40) + return false; + + int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); + if (choice == IDOK) { + /* Open the URL in the default browser. */ + ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); + } + + return true; +} +#endif + +int main(int argc, char *argv[]) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); + + struct sigaction sig_handler; + + sig_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sig_handler.sa_mask); + sig_handler.sa_flags = 0; + + sigaction(SIGINT, &sig_handler, NULL); + + /* Block SIGPIPE in all threads, this can happen if a thread calls write on + a closed pipe. */ + sigset_t sigpipe_mask; + sigemptyset(&sigpipe_mask); + sigaddset(&sigpipe_mask, SIGPIPE); + sigset_t saved_mask; + if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { + perror("pthread_sigmask"); + exit(1); + } +#endif + +#ifdef _WIN32 + // Abort as early as possible if MSVC runtime is outdated + if (vc_runtime_outdated()) + return 1; + // Try to keep this as early as possible + install_dll_blocklist_hook(); + + obs_init_win32_crash_handler(); + SetErrorMode(SEM_FAILCRITICALERRORS); + load_debug_privilege(); + base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } +#endif + + base_get_log_handler(&def_log_handler, nullptr); + +#if defined(__FreeBSD__) + move_to_xdg(); +#endif + + obs_set_cmdline_args(argc, argv); + + for (int i = 1; i < argc; i++) { + if (arg_is(argv[i], "--multi", "-m")) { + multi = true; + disable_shutdown_check = true; + +#if ALLOW_PORTABLE_MODE + } else if (arg_is(argv[i], "--portable", "-p")) { + portable_mode = true; + +#endif + } else if (arg_is(argv[i], "--verbose", nullptr)) { + log_verbose = true; + + } else if (arg_is(argv[i], "--safe-mode", nullptr)) { + safe_mode = true; + + } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { + disable_3p_plugins = true; + + } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { + /* This exists mostly to bypass the dialog during development. */ + disable_shutdown_check = true; + + } else if (arg_is(argv[i], "--always-on-top", nullptr)) { + opt_always_on_top = true; + + } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { + unfiltered_log = true; + + } else if (arg_is(argv[i], "--startstreaming", nullptr)) { + opt_start_streaming = true; + + } else if (arg_is(argv[i], "--startrecording", nullptr)) { + opt_start_recording = true; + + } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { + opt_start_replaybuffer = true; + + } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { + opt_start_virtualcam = true; + + } else if (arg_is(argv[i], "--collection", nullptr)) { + if (++i < argc) + opt_starting_collection = argv[i]; + + } else if (arg_is(argv[i], "--profile", nullptr)) { + if (++i < argc) + opt_starting_profile = argv[i]; + + } else if (arg_is(argv[i], "--scene", nullptr)) { + if (++i < argc) + opt_starting_scene = argv[i]; + + } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { + opt_minimize_tray = true; + + } else if (arg_is(argv[i], "--studio-mode", nullptr)) { + opt_studio_mode = true; + + } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { + opt_allow_opengl = true; + + } else if (arg_is(argv[i], "--disable-updater", nullptr)) { + opt_disable_updater = true; + + } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { + opt_disable_missing_files_check = true; + + } else if (arg_is(argv[i], "--steam", nullptr)) { + steam = true; + + } else if (arg_is(argv[i], "--help", "-h")) { + std::string help = + "--help, -h: Get list of available commands.\n\n" + "--startstreaming: Automatically start streaming.\n" + "--startrecording: Automatically start recording.\n" + "--startreplaybuffer: Start replay buffer.\n" + "--startvirtualcam: Start virtual camera (if available).\n\n" + "--collection : Use specific scene collection." + "\n" + "--profile : Use specific profile.\n" + "--scene : Start with specific scene.\n\n" + "--studio-mode: Enable studio mode.\n" + "--minimize-to-tray: Minimize to system tray.\n" +#if ALLOW_PORTABLE_MODE + "--portable, -p: Use portable mode.\n" +#endif + "--multi, -m: Don't warn when launching multiple instances.\n\n" + "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" + "--only-bundled-plugins: Only load included (first-party) plugins\n" + "--disable-shutdown-check: Disable unclean shutdown detection.\n" + "--verbose: Make log more verbose.\n" + "--always-on-top: Start in 'always on top' mode.\n\n" + "--unfiltered_log: Make log unfiltered.\n\n" + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" + "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + +#ifdef _WIN32 + MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); +#else + std::cout << help << "--version, -V: Get current version.\n"; +#endif + exit(0); + + } else if (arg_is(argv[i], "--version", "-V")) { + std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; + exit(0); + } + } + +#if ALLOW_PORTABLE_MODE + if (!portable_mode) { + portable_mode = os_file_exists(BASE_PATH "/portable_mode") || + os_file_exists(BASE_PATH "/obs_portable_mode") || + os_file_exists(BASE_PATH "/portable_mode.txt") || + os_file_exists(BASE_PATH "/obs_portable_mode.txt"); + } + + if (!opt_disable_updater) { + opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || + os_file_exists(BASE_PATH "/disable_updater.txt"); + } + + if (!opt_disable_missing_files_check) { + opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || + os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); + } +#endif + + check_safe_mode_sentinel(); + + fstream logFile; + + curl_global_init(CURL_GLOBAL_ALL); + int ret = run_program(logFile, argc, argv); + +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } + + log_blocked_dlls(); +#endif + + delete_safe_mode_sentinel(); + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); + base_set_log_handler(nullptr, nullptr); + + if (restart || restart_safe) { + auto executable = arguments.takeFirst(); + QProcess::startDetached(executable, arguments); + } + + return ret; +} diff --git a/frontend/utility/OBSTranslator.hpp b/frontend/utility/OBSTranslator.hpp new file mode 100644 index 000000000..dea5e530b --- /dev/null +++ b/frontend/utility/OBSTranslator.hpp @@ -0,0 +1,280 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + 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 +#include +#include +#include + +#ifndef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-main.hpp" +#include "obs-app-theming.hpp" + +std::string CurrentTimeString(); +std::string CurrentDateTimeString(); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); + +struct BaseLexer { + lexer lex; + +public: + inline BaseLexer() { lexer_init(&lex); } + inline ~BaseLexer() { lexer_free(&lex); } + operator lexer *() { return &lex; } +}; + +class OBSTranslator : public QTranslator { + Q_OBJECT + +public: + virtual bool isEmpty() const override { return false; } + + virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, + int n) const override; +}; + +typedef std::function VoidFunc; + +struct UpdateBranch { + QString name; + QString display_name; + QString description; + bool is_enabled; + bool is_visible; +}; + +class OBSApp : public QApplication { + Q_OBJECT + +private: + std::string locale; + + ConfigFile appConfig; + ConfigFile userConfig; + TextLookup textLookup; + QPointer mainWindow; + profiler_name_store_t *profilerNameStore = nullptr; + std::vector updateBranches; + bool branches_loaded = false; + + bool libobs_initialized = false; + + os_inhibit_t *sleepInhibitor = nullptr; + int sleepInhibitRefs = 0; + + bool enableHotkeysInFocus = true; + bool enableHotkeysOutOfFocus = true; + + std::deque translatorHooks; + + bool UpdatePre22MultiviewLayout(const char *layout); + + bool InitGlobalConfig(); + bool InitGlobalConfigDefaults(); + bool InitGlobalLocationDefaults(); + + bool MigrateGlobalSettings(); + void MigrateLegacySettings(uint32_t lastVersion); + + bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); + void InitUserConfigDefaults(); + + bool InitLocale(); + bool InitTheme(); + + inline void ResetHotkeyState(bool inFocus); + + QPalette defaultPalette; + OBSTheme *currentTheme = nullptr; + QHash themes; + QPointer themeWatcher; + + void FindThemes(); + + bool notify(QObject *receiver, QEvent *e) override; + +#ifndef _WIN32 + static int sigintFd[2]; + QSocketNotifier *snInt = nullptr; +#else +private slots: + void commitData(QSessionManager &manager); +#endif + +private slots: + void themeFileChanged(const QString &); + +public: + OBSApp(int &argc, char **argv, profiler_name_store_t *store); + ~OBSApp(); + + void AppInit(); + bool OBSInit(); + + void UpdateHotkeyFocusSetting(bool reset = true); + void DisableHotkeys(); + + inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } + + inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } + + inline config_t *GetAppConfig() const { return appConfig; } + inline config_t *GetUserConfig() const { return userConfig; } + std::filesystem::path userConfigLocation; + std::filesystem::path userScenesLocation; + std::filesystem::path userProfilesLocation; + + inline const char *GetLocale() const { return locale.c_str(); } + + OBSTheme *GetTheme() const { return currentTheme; } + QList GetThemes() const { return themes.values(); } + OBSTheme *GetTheme(const QString &name); + bool SetTheme(const QString &name); + bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } + + void SetBranchData(const std::string &data); + std::vector GetBranches(); + + inline lookup_t *GetTextLookup() const { return textLookup; } + + inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } + + bool TranslateString(const char *lookupVal, const char **out) const; + + profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } + + const char *GetLastLog() const; + const char *GetCurrentLog() const; + + const char *GetLastCrashLog() const; + + std::string GetVersionString(bool platform = true) const; + bool IsPortableMode(); + bool IsUpdaterDisabled(); + bool IsMissingFilesCheckDisabled(); + + const char *InputAudioSource() const; + const char *OutputAudioSource() const; + + const char *GetRenderModule() const; + + inline void IncrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs++ == 0) + os_inhibit_sleep_set_active(sleepInhibitor, true); + } + + inline void DecrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs == 0) + return; + if (--sleepInhibitRefs == 0) + os_inhibit_sleep_set_active(sleepInhibitor, false); + } + + inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } + + inline void PopUITranslation() { translatorHooks.pop_front(); } +#ifndef _WIN32 + static void SigIntSignalHandler(int); +#endif + +public slots: + void Exec(VoidFunc func); + void ProcessSigInt(); + +signals: + void StyleChanged(); +}; + +int GetAppConfigPath(char *path, size_t size, const char *name); +char *GetAppConfigPathPtr(const char *name); + +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + +inline OBSApp *App() +{ + return static_cast(qApp); +} + +std::vector> GetLocaleNames(); +inline const char *Str(const char *lookup) +{ + return App()->GetString(lookup); +} +inline QString QTStr(const char *lookupVal) +{ + return QString::fromUtf8(Str(lookupVal)); +} + +bool GetFileSafeName(const char *name, std::string &file); +bool GetClosestUnusedFileName(std::string &path, const char *extension); +bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); + +bool WindowPositionValid(QRect rect); + +extern bool portable_mode; +extern bool steam; +extern bool safe_mode; +extern bool disable_3p_plugins; + +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; +extern bool opt_minimize_tray; +extern bool opt_studio_mode; +extern bool opt_allow_opengl; +extern bool opt_always_on_top; +extern std::string opt_starting_scene; +extern bool restart; +extern bool restart_safe; + +#ifdef _WIN32 +extern "C" void install_dll_blocklist_hook(void); +extern "C" void log_blocked_dlls(void); +#endif From 2be464a21fde5892236e4b48986f3829c59316f8 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 11 Dec 2024 19:03:30 +0100 Subject: [PATCH 27/37] frontend: Split main application implementation into single files --- frontend/OBSApp.cpp | 1165 +---------- frontend/OBSApp.hpp | 97 +- frontend/OBSApp_Themes.cpp | 24 +- frontend/OBSStudioAPI.cpp | 1160 ++++++----- frontend/OBSStudioAPI.hpp | 782 ++------ frontend/obs-main.cpp | 1616 +-------------- frontend/utility/BaseLexer.hpp | 251 --- frontend/utility/OBSTheme.hpp | 26 +- frontend/utility/OBSThemeVariable.hpp | 26 +- frontend/utility/OBSTranslator.cpp | 2637 +------------------------ frontend/utility/OBSTranslator.hpp | 251 +-- 11 files changed, 940 insertions(+), 7095 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 883bee21a..36f317728 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -15,108 +15,59 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "obs-proxy-style.hpp" -#include "log-viewer.hpp" -#include "volume-control.hpp" -#include "window-basic-main.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-basic-settings.hpp" -#include "platform.hpp" - -#include - -#include - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#include -#include -#include -#endif +#include "OBSApp.hpp" +#include +#include #if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -#include "update/models/branches.hpp" +#include #endif +#include #if !defined(_WIN32) && !defined(__APPLE__) #include +#endif +#include + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include +#endif + +#ifdef _WIN32 +#include +#else +#include +#endif +#if !defined(_WIN32) && !defined(__APPLE__) #include #endif -#include +#ifdef _WIN32 +#include +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif -#include "ui-config.h" +#include "moc_OBSApp.cpp" using namespace std; -static log_handler_t def_log_handler; +string currentLogFile; +string lastLogFile; +string lastCrashLogFile; -static string currentLogFile; -static string lastLogFile; -static string lastCrashLogFile; +extern bool portable_mode; +extern bool safe_mode; +extern bool disable_3p_plugins; +extern bool opt_disable_updater; +extern bool opt_disable_missing_files_check; +extern string opt_starting_collection; +extern string opt_starting_profile; -bool portable_mode = false; -bool steam = false; -bool safe_mode = false; -bool disable_3p_plugins = false; -bool unclean_shutdown = false; -bool disable_shutdown_check = false; -static bool multi = false; -static bool log_verbose = false; -static bool unfiltered_log = false; -bool opt_start_streaming = false; -bool opt_start_recording = false; -bool opt_studio_mode = false; -bool opt_start_replaybuffer = false; -bool opt_start_virtualcam = false; -bool opt_minimize_tray = false; -bool opt_allow_opengl = false; -bool opt_always_on_top = false; -bool opt_disable_updater = false; -bool opt_disable_missing_files_check = false; -string opt_starting_collection; -string opt_starting_profile; -string opt_starting_scene; - -bool restart = false; -bool restart_safe = false; -QStringList arguments; - -QPointer obsLogViewer; +extern QPointer obsLogViewer; #ifndef _WIN32 int OBSApp::sigintFd[2]; @@ -252,28 +203,6 @@ QObject *CreateShortcutFilter() }); } -string CurrentTimeString() -{ - using namespace std::chrono; - - struct tm tstruct; - char buf[80]; - - auto tp = system_clock::now(); - auto now = system_clock::to_time_t(tp); - tstruct = *localtime(&now); - - size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); - if (ratio_less::value && written && (sizeof(buf) - written) > 5) { - auto tp_secs = time_point_cast(tp); - auto millis = duration_cast(tp - tp_secs).count(); - - snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); - } - - return buf; -} - string CurrentDateTimeString() { time_t now = time(0); @@ -284,145 +213,6 @@ string CurrentDateTimeString() return buf; } -static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) -{ - static mutex logfile_mutex; - string msg; - msg += timeString; - msg += str; - - logfile_mutex.lock(); - logFile << msg << endl; - logfile_mutex.unlock(); - - if (!!obsLogViewer) - QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), - Q_ARG(QString, QString(msg.c_str()))); -} - -static inline void LogStringChunk(fstream &logFile, char *str, int log_level) -{ - char *nextLine = str; - string timeString = CurrentTimeString(); - timeString += ": "; - - while (*nextLine) { - char *nextLine = strchr(str, '\n'); - if (!nextLine) - break; - - if (nextLine != str && nextLine[-1] == '\r') { - nextLine[-1] = 0; - } else { - nextLine[0] = 0; - } - - LogString(logFile, timeString.c_str(), str, log_level); - nextLine++; - str = nextLine; - } - - LogString(logFile, timeString.c_str(), str, log_level); -} - -#define MAX_REPEATED_LINES 30 -#define MAX_CHAR_VARIATION (255 * 3) - -static inline int sum_chars(const char *str) -{ - int val = 0; - for (; *str != 0; str++) - val += *str; - - return val; -} - -static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) -{ - static mutex log_mutex; - static const char *last_msg_ptr = nullptr; - static int last_char_sum = 0; - static int rep_count = 0; - - int new_sum = sum_chars(output_str); - - lock_guard guard(log_mutex); - - if (unfiltered_log) { - return false; - } - - if (last_msg_ptr == msg) { - int diff = std::abs(new_sum - last_char_sum); - if (diff < MAX_CHAR_VARIATION) { - return (rep_count++ >= MAX_REPEATED_LINES); - } - } - - if (rep_count > MAX_REPEATED_LINES) { - logFile << CurrentTimeString() << ": Last log entry repeated for " - << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; - } - - last_msg_ptr = msg; - last_char_sum = new_sum; - rep_count = 0; - - return false; -} - -static void do_log(int log_level, const char *msg, va_list args, void *param) -{ - fstream &logFile = *static_cast(param); - char str[8192]; - -#ifndef _WIN32 - va_list args2; - va_copy(args2, args); -#endif - - vsnprintf(str, sizeof(str), msg, args); - -#ifdef _WIN32 - if (IsDebuggerPresent()) { - int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); - if (wNum > 1) { - static wstring wide_buf; - static mutex wide_mutex; - - lock_guard lock(wide_mutex); - wide_buf.reserve(wNum + 1); - wide_buf.resize(wNum - 1); - MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); - wide_buf.push_back('\n'); - - OutputDebugStringW(wide_buf.c_str()); - } - } -#endif - -#if !defined(_WIN32) && defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - - if (log_level <= LOG_INFO || log_verbose) { -#if !defined(_WIN32) && !defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - if (!too_many_repeated_entries(logFile, msg, str)) - LogStringChunk(logFile, str, log_level); - } - -#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) - if (log_level <= LOG_ERROR && IsDebuggerPresent()) - __debugbreak(); -#endif - -#ifndef _WIN32 - va_end(args2); -#endif -} - #define DEFAULT_LANG "en-US" bool OBSApp::InitGlobalConfigDefaults() @@ -1214,8 +1004,6 @@ void OBSApp::DisableHotkeys() ResetHotkeyState(applicationState() == Qt::ApplicationActive); } -Q_DECLARE_METATYPE(VoidFunc) - void OBSApp::Exec(VoidFunc func) { func(); @@ -1437,172 +1225,6 @@ skip: return QApplication::notify(receiver, e); } -QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const -{ - const char *out = nullptr; - QString str(sourceText); - str.replace(" ", ""); - if (!App()->TranslateString(QT_TO_UTF8(str), &out)) - return QString(sourceText); - - return QT_UTF8(out); -} - -static bool get_token(lexer *lex, string &str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - str.assign(token.text.array, token.text.len); - return true; -} - -static bool expect_token(lexer *lex, const char *str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - return strref_cmp(&token.text, str) == 0; -} - -static uint64_t convert_log_name(bool has_prefix, const char *name) -{ - BaseLexer lex; - string year, month, day, hour, minute, second; - - lexer_start(lex, name); - - if (has_prefix) { - string temp; - if (!get_token(lex, temp, BASETOKEN_ALPHA)) - return 0; - } - - if (!get_token(lex, year, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, month, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, day, BASETOKEN_DIGIT)) - return 0; - if (!get_token(lex, hour, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, minute, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, second, BASETOKEN_DIGIT)) - return 0; - - stringstream timestring; - timestring << year << month << day << hour << minute << second; - return std::stoull(timestring.str()); -} - -/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ -/* TODO: Remove after version 32.0. */ -#if defined(__FreeBSD__) -static void move_to_xdg(void) -{ - char old_path[512]; - char new_path[512]; - char *home = getenv("HOME"); - if (!home) - return; - - if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) - return; - - /* make base xdg path if it doesn't already exist */ - if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) - return; - if (os_mkdirs(new_path) == MKDIR_ERROR) - return; - - if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) - return; - - if (os_file_exists(old_path) && !os_file_exists(new_path)) { - rename(old_path, new_path); - } -} -#endif - -static void delete_oldest_file(bool has_prefix, const char *location) -{ - BPtr logDir(GetAppConfigPathPtr(location)); - string oldestLog; - uint64_t oldest_ts = (uint64_t)-1; - struct os_dirent *entry; - - unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); - - os_dir_t *dir = os_opendir(logDir); - if (dir) { - unsigned int count = 0; - - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts) { - if (ts < oldest_ts) { - oldestLog = entry->d_name; - oldest_ts = ts; - } - - count++; - } - } - - os_closedir(dir); - - if (count > maxLogs) { - stringstream delPath; - - delPath << logDir << "/" << oldestLog; - os_unlink(delPath.str().c_str()); - } - } -} - -static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) -{ - BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); - struct os_dirent *entry; - os_dir_t *dir = os_opendir(logDir); - uint64_t highest_ts = 0; - - if (dir) { - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts > highest_ts) { - last = entry->d_name; - highest_ts = ts; - } - } - - os_closedir(dir); - } -} - string GenerateTimeDateFilename(const char *extension, bool noSpace) { time_t now = time(0); @@ -1786,444 +1408,7 @@ vector> GetLocaleNames() return names; } -static void create_log_file(fstream &logFile) -{ - stringstream dst; - - get_last_log(false, "obs-studio/logs", lastLogFile); -#ifdef _WIN32 - get_last_log(true, "obs-studio/crashes", lastCrashLogFile); -#endif - - currentLogFile = GenerateTimeDateFilename("txt"); - dst << "obs-studio/logs/" << currentLogFile.c_str(); - - BPtr path(GetAppConfigPathPtr(dst.str().c_str())); - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); -#else - logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); -#endif - - if (logFile.is_open()) { - delete_oldest_file(false, "obs-studio/logs"); - base_set_log_handler(do_log, &logFile); - } else { - blog(LOG_ERROR, "Failed to open log file"); - } -} - -static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { - profiler_name_store_free(store); -}; - -using ProfilerNameStore = std::unique_ptr; - -ProfilerNameStore CreateNameStore() -{ - return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; -} - -static auto SnapshotRelease = [](profiler_snapshot_t *snap) { - profile_snapshot_free(snap); -}; - -using ProfilerSnapshot = std::unique_ptr; - -ProfilerSnapshot GetSnapshot() -{ - return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; -} - -static void SaveProfilerData(const ProfilerSnapshot &snap) -{ - if (currentLogFile.empty()) - return; - - auto pos = currentLogFile.rfind('.'); - if (pos == currentLogFile.npos) - return; - -#define LITERAL_SIZE(x) x, (sizeof(x) - 1) - ostringstream dst; - dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); - dst.write(currentLogFile.c_str(), pos); - dst.write(LITERAL_SIZE(".csv.gz")); -#undef LITERAL_SIZE - - BPtr path = GetAppConfigPathPtr(dst.str().c_str()); - if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) - blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); -} - -static auto ProfilerFree = [](void *) { - profiler_stop(); - - auto snap = GetSnapshot(); - - profiler_print(snap.get()); - profiler_print_time_between_calls(snap.get()); - - SaveProfilerData(snap); - - profiler_free(); -}; - -QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) -{ - if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) - return new VolumeAccessibleInterface(static_cast(object)); - - return nullptr; -} - -static const char *run_program_init = "run_program_init"; -static int run_program(fstream &logFile, int argc, char *argv[]) -{ - int ret = -1; - - auto profilerNameStore = CreateNameStore(); - - std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); - - profiler_start(); - profile_register_root(run_program_init, 0); - - ScopeProfiler prof{run_program_init}; - -#ifdef _WIN32 - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - - QCoreApplication::addLibraryPath("."); - -#if __APPLE__ - InstallNSApplicationSubclass(); - InstallNSThreadLocks(); - - if (!isInBundle()) { - blog(LOG_ERROR, - "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); - return ret; - } -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) - /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and - * crashes loading saved geometry. Just turn off this theme and let users complain OBS - * looks ugly instead of crashing. */ - const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); - if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) - unsetenv("QT_QPA_PLATFORMTHEME"); -#endif - - /* NOTE: This disables an optimisation in Qt that attempts to determine if - * any "siblings" intersect with a widget when determining the approximate - * visible/unobscured area. However, by Qt's own admission this is slow - * and in the case of OBS it significantly slows down lists with many - * elements (e.g. Hotkeys) and it is actually faster to disable it. */ - qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); - - OBSApp program(argc, argv, profilerNameStore.get()); - try { - QAccessible::installFactory(accessibleFactory); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); - - bool created_log = false; - - program.AppInit(); - delete_oldest_file(false, "obs-studio/profiler_data"); - - OBSTranslator translator; - program.installTranslator(&translator); - - /* --------------------------------------- */ - /* check and warn if already running */ - - bool cancel_launch = false; - bool already_running = false; - -#ifdef _WIN32 - RunOnceMutex rom = -#endif - CheckIfAlreadyRunning(already_running); - - if (!already_running) { - goto run; - } - - if (!multi) { - QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), - QTStr("AlreadyRunning.Text")); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); - QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); - mb.setDefaultButton(cancelButton); - - mb.exec(); - cancel_launch = mb.clickedButton() == cancelButton; - } - - if (cancel_launch) - return 0; - - if (!created_log) { - create_log_file(logFile); - created_log = true; - } - - if (multi) { - blog(LOG_INFO, "User enabled --multi flag and is now " - "running multiple instances of OBS."); - } else { - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "Warning: OBS is already running!"); - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "User is now running multiple " - "instances of OBS!"); - /* Clear unclean_shutdown flag as multiple instances - * running from the same config will lead to a - * false-positive detection.*/ - unclean_shutdown = false; - } - - /* --------------------------------------- */ - run: - -#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) - // Mounted by termina during chromeOS linux container startup - // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh - os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); - if (crosDir) { - QMessageBox::StandardButtons buttons(QMessageBox::Ok); - QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, - nullptr); - - mb.exec(); - return 0; - } -#endif - - if (!created_log) - create_log_file(logFile); - - if (unclean_shutdown) { - blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); - } - - if (unclean_shutdown && !safe_mode) { - QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); - QPushButton *launchSafeButton = - mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); - QPushButton *launchNormalButton = - mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); - mb.setDefaultButton(launchNormalButton); - mb.exec(); - - safe_mode = mb.clickedButton() == launchSafeButton; - if (safe_mode) { - blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); - } else { - blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); - } - } - - qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { - switch (type) { -#ifdef _DEBUG - case QtDebugMsg: - blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); - break; - case QtInfoMsg: - blog(LOG_INFO, "%s", QT_TO_UTF8(message)); - break; -#else - case QtDebugMsg: - case QtInfoMsg: - break; -#endif - case QtWarningMsg: - blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); - break; - case QtCriticalMsg: - case QtFatalMsg: - blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); - break; - } - }); - -#ifdef __APPLE__ - MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); - MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); - MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); - MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); - - int permissionsDialogLastShown = - config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); - if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { - OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, - accessibility_permission); - check.exec(); - } -#endif - -#ifdef _WIN32 - if (IsRunningOnWine()) { - QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); - mb.setTextFormat(Qt::RichText); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); - QPushButton *closeButton = mb.addButton(QMessageBox::Close); - mb.setDefaultButton(closeButton); - - mb.exec(); - if (mb.clickedButton() == closeButton) - return 0; - } -#endif - - if (argc > 1) { - stringstream stor; - stor << argv[1]; - for (int i = 2; i < argc; ++i) { - stor << " " << argv[i]; - } - blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); - } - - if (!program.OBSInit()) - return 0; - - prof.Stop(); - - ret = program.exec(); - - } catch (const char *error) { - blog(LOG_ERROR, "%s", error); - OBSErrorBox(nullptr, "%s", error); - } - - if (restart || restart_safe) { - arguments = qApp->arguments(); - - if (restart_safe) { - arguments.append("--safe-mode"); - } else { - arguments.removeAll("--safe-mode"); - } - } - - return ret; -} - -#define MAX_CRASH_REPORT_SIZE (150 * 1024) - -#ifdef _WIN32 - -#define CRASH_MESSAGE \ - "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ - "to the clipboard? The crash log will still be saved to:\n\n%s" - -static void main_crash_handler(const char *format, va_list args, void * /* param */) -{ - char *text = new char[MAX_CRASH_REPORT_SIZE]; - - vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); - text[MAX_CRASH_REPORT_SIZE - 1] = 0; - - string crashFilePath = "obs-studio/crashes"; - - delete_oldest_file(true, crashFilePath.c_str()); - - string name = crashFilePath + "/"; - name += "Crash " + GenerateTimeDateFilename("txt"); - - BPtr path(GetAppConfigPathPtr(name.c_str())); - - fstream file; - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#else - file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#endif - file << text; - file.close(); - - string pathString(path.Get()); - -#ifdef _WIN32 - std::replace(pathString.begin(), pathString.end(), '/', '\\'); -#endif - - string absolutePath = canonical(filesystem::path(pathString)).u8string(); - - size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); - - unique_ptr message_buffer(new char[size + 1]); - - snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); - - string finalMessage = string(message_buffer.get(), message_buffer.get() + size); - - int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); - - if (ret == IDYES) { - size_t len = strlen(text); - - HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); - memcpy(GlobalLock(mem), text, len); - GlobalUnlock(mem); - - OpenClipboard(0); - EmptyClipboard(); - SetClipboardData(CF_TEXT, mem); - CloseClipboard(); - } - - exit(-1); -} - -static void load_debug_privilege(void) -{ - const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; - TOKEN_PRIVILEGES tp; - HANDLE token; - LUID val; - - if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { - return; - } - - if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); - } - - if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { - blog(LOG_INFO, "Could not set privilege to " - "increase GPU priority"); - } - } - - CloseHandle(token); -} -#endif - -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__linux__) #define BASE_PATH ".." #else #define BASE_PATH "../.." @@ -2351,41 +1536,6 @@ bool WindowPositionValid(QRect rect) return false; } -static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) -{ - return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); -} - -static void check_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - /* Safe Mode detection is disabled in Debug builds to keep developers - * somewhat sane. */ - return; -#else - if (disable_shutdown_check) - return; - - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - if (os_file_exists(sentinelPath)) { - unclean_shutdown = true; - return; - } - - os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); -#endif -} - -static void delete_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - return; -#else - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - os_unlink(sentinelPath); -#endif -} - #ifndef _WIN32 void OBSApp::SigIntSignalHandler(int s) { @@ -2423,240 +1573,3 @@ void OBSApp::commitData(QSessionManager &manager) } } #endif - -#ifdef _WIN32 -static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; -static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " - "Redistributables.\n\nYou will now be directed to the download page."; -static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; - -static bool vc_runtime_outdated() -{ - win_version_info ver; - if (!get_dll_ver(L"msvcp140.dll", &ver)) - return true; - /* Major is always 14 (hence 140.dll), so we only care about minor. */ - if (ver.minor >= 40) - return false; - - int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); - if (choice == IDOK) { - /* Open the URL in the default browser. */ - ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); - } - - return true; -} -#endif - -int main(int argc, char *argv[]) -{ -#ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); - - struct sigaction sig_handler; - - sig_handler.sa_handler = OBSApp::SigIntSignalHandler; - sigemptyset(&sig_handler.sa_mask); - sig_handler.sa_flags = 0; - - sigaction(SIGINT, &sig_handler, NULL); - - /* Block SIGPIPE in all threads, this can happen if a thread calls write on - a closed pipe. */ - sigset_t sigpipe_mask; - sigemptyset(&sigpipe_mask); - sigaddset(&sigpipe_mask, SIGPIPE); - sigset_t saved_mask; - if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { - perror("pthread_sigmask"); - exit(1); - } -#endif - -#ifdef _WIN32 - // Abort as early as possible if MSVC runtime is outdated - if (vc_runtime_outdated()) - return 1; - // Try to keep this as early as possible - install_dll_blocklist_hook(); - - obs_init_win32_crash_handler(); - SetErrorMode(SEM_FAILCRITICALERRORS); - load_debug_privilege(); - base_set_crash_handler(main_crash_handler, nullptr); - - const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); - PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); - func(); - } -#endif - - base_get_log_handler(&def_log_handler, nullptr); - -#if defined(__FreeBSD__) - move_to_xdg(); -#endif - - obs_set_cmdline_args(argc, argv); - - for (int i = 1; i < argc; i++) { - if (arg_is(argv[i], "--multi", "-m")) { - multi = true; - disable_shutdown_check = true; - -#if ALLOW_PORTABLE_MODE - } else if (arg_is(argv[i], "--portable", "-p")) { - portable_mode = true; - -#endif - } else if (arg_is(argv[i], "--verbose", nullptr)) { - log_verbose = true; - - } else if (arg_is(argv[i], "--safe-mode", nullptr)) { - safe_mode = true; - - } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { - disable_3p_plugins = true; - - } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { - /* This exists mostly to bypass the dialog during development. */ - disable_shutdown_check = true; - - } else if (arg_is(argv[i], "--always-on-top", nullptr)) { - opt_always_on_top = true; - - } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { - unfiltered_log = true; - - } else if (arg_is(argv[i], "--startstreaming", nullptr)) { - opt_start_streaming = true; - - } else if (arg_is(argv[i], "--startrecording", nullptr)) { - opt_start_recording = true; - - } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { - opt_start_replaybuffer = true; - - } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { - opt_start_virtualcam = true; - - } else if (arg_is(argv[i], "--collection", nullptr)) { - if (++i < argc) - opt_starting_collection = argv[i]; - - } else if (arg_is(argv[i], "--profile", nullptr)) { - if (++i < argc) - opt_starting_profile = argv[i]; - - } else if (arg_is(argv[i], "--scene", nullptr)) { - if (++i < argc) - opt_starting_scene = argv[i]; - - } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { - opt_minimize_tray = true; - - } else if (arg_is(argv[i], "--studio-mode", nullptr)) { - opt_studio_mode = true; - - } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { - opt_allow_opengl = true; - - } else if (arg_is(argv[i], "--disable-updater", nullptr)) { - opt_disable_updater = true; - - } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { - opt_disable_missing_files_check = true; - - } else if (arg_is(argv[i], "--steam", nullptr)) { - steam = true; - - } else if (arg_is(argv[i], "--help", "-h")) { - std::string help = - "--help, -h: Get list of available commands.\n\n" - "--startstreaming: Automatically start streaming.\n" - "--startrecording: Automatically start recording.\n" - "--startreplaybuffer: Start replay buffer.\n" - "--startvirtualcam: Start virtual camera (if available).\n\n" - "--collection : Use specific scene collection." - "\n" - "--profile : Use specific profile.\n" - "--scene : Start with specific scene.\n\n" - "--studio-mode: Enable studio mode.\n" - "--minimize-to-tray: Minimize to system tray.\n" -#if ALLOW_PORTABLE_MODE - "--portable, -p: Use portable mode.\n" -#endif - "--multi, -m: Don't warn when launching multiple instances.\n\n" - "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" - "--only-bundled-plugins: Only load included (first-party) plugins\n" - "--disable-shutdown-check: Disable unclean shutdown detection.\n" - "--verbose: Make log more verbose.\n" - "--always-on-top: Start in 'always on top' mode.\n\n" - "--unfiltered_log: Make log unfiltered.\n\n" - "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" - "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; - -#ifdef _WIN32 - MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); -#else - std::cout << help << "--version, -V: Get current version.\n"; -#endif - exit(0); - - } else if (arg_is(argv[i], "--version", "-V")) { - std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; - exit(0); - } - } - -#if ALLOW_PORTABLE_MODE - if (!portable_mode) { - portable_mode = os_file_exists(BASE_PATH "/portable_mode") || - os_file_exists(BASE_PATH "/obs_portable_mode") || - os_file_exists(BASE_PATH "/portable_mode.txt") || - os_file_exists(BASE_PATH "/obs_portable_mode.txt"); - } - - if (!opt_disable_updater) { - opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || - os_file_exists(BASE_PATH "/disable_updater.txt"); - } - - if (!opt_disable_missing_files_check) { - opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || - os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); - } -#endif - - check_safe_mode_sentinel(); - - fstream logFile; - - curl_global_init(CURL_GLOBAL_ALL); - int ret = run_program(logFile, argc, argv); - -#ifdef _WIN32 - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); - PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); - func(); - FreeLibrary(hRtwq); - } - - log_blocked_dlls(); -#endif - - delete_safe_mode_sentinel(); - blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); - base_set_log_handler(nullptr, nullptr); - - if (restart || restart_safe) { - auto executable = arguments.takeFirst(); - QProcess::startDetached(executable, arguments); - } - - return ret; -} diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index dea5e530b..ce0c4fb65 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -17,63 +17,30 @@ #pragma once -#include -#include -#include -#include +#include +#include -#ifndef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include #include +#include +#include +#include + +#include +#include +#include + +#include #include #include -#include #include -#include -#include - -#include "window-main.hpp" -#include "obs-app-theming.hpp" - -std::string CurrentTimeString(); -std::string CurrentDateTimeString(); -std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); -std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); -std::string GetFormatString(const char *format, const char *prefix, const char *suffix); -std::string GetFormatExt(const char *container); -std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, - const char *format); -QObject *CreateShortcutFilter(); - -struct BaseLexer { - lexer lex; - -public: - inline BaseLexer() { lexer_init(&lex); } - inline ~BaseLexer() { lexer_free(&lex); } - operator lexer *() { return &lex; } -}; - -class OBSTranslator : public QTranslator { - Q_OBJECT - -public: - virtual bool isEmpty() const override { return false; } - - virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, - int n) const override; -}; typedef std::function VoidFunc; +Q_DECLARE_METATYPE(VoidFunc) + +class QFileSystemWatcher; +class QSocketNotifier; + struct UpdateBranch { QString name; QString display_name; @@ -233,9 +200,6 @@ signals: int GetAppConfigPath(char *path, size_t size, const char *name); char *GetAppConfigPathPtr(const char *name); -int GetProgramDataPath(char *path, size_t size, const char *name); -char *GetProgramDataPathPtr(const char *name); - inline OBSApp *App() { return static_cast(qApp); @@ -251,30 +215,23 @@ inline QString QTStr(const char *lookupVal) return QString::fromUtf8(Str(lookupVal)); } +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + bool GetFileSafeName(const char *name, std::string &file); bool GetClosestUnusedFileName(std::string &path, const char *extension); -bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); bool WindowPositionValid(QRect rect); -extern bool portable_mode; -extern bool steam; -extern bool safe_mode; -extern bool disable_3p_plugins; - -extern bool opt_start_streaming; -extern bool opt_start_recording; -extern bool opt_start_replaybuffer; -extern bool opt_start_virtualcam; -extern bool opt_minimize_tray; -extern bool opt_studio_mode; -extern bool opt_allow_opengl; -extern bool opt_always_on_top; -extern std::string opt_starting_scene; -extern bool restart; -extern bool restart_safe; - #ifdef _WIN32 extern "C" void install_dll_blocklist_hook(void); extern "C" void log_blocked_dlls(void); #endif + +std::string CurrentDateTimeString(); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); diff --git a/frontend/OBSApp_Themes.cpp b/frontend/OBSApp_Themes.cpp index 9698ac455..b4206bb66 100644 --- a/frontend/OBSApp_Themes.cpp +++ b/frontend/OBSApp_Themes.cpp @@ -15,25 +15,23 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "OBSApp.hpp" +#include +#include +#include + +#include +#include #include #include -#include -#include -#include #include -#include +#include +#include +#include #include - -#include "qt-wrappers.hpp" -#include "obs-app.hpp" -#include "obs-app-theming.hpp" -#include "obs-proxy-style.hpp" -#include "platform.hpp" - -#include "ui-config.h" +#include using namespace std; diff --git a/frontend/OBSStudioAPI.cpp b/frontend/OBSStudioAPI.cpp index bb3715017..6e84fdaed 100644 --- a/frontend/OBSStudioAPI.cpp +++ b/frontend/OBSStudioAPI.cpp @@ -1,20 +1,9 @@ -#include +#include "OBSStudioAPI.hpp" + +#include +#include + #include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" - -#include - -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} extern volatile bool streaming_active; extern volatile bool recording_active; @@ -22,15 +11,6 @@ extern volatile bool recording_paused; extern volatile bool replaybuf_active; extern volatile bool virtualcam_active; -/* ------------------------------------------------------------------------- */ - -template struct OBSStudioCallback { - T callback; - void *private_data; - - inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} -}; - template inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) { @@ -43,590 +23,700 @@ inline size_t GetCallbackIdx(vector> &callbacks, T callback return (size_t)-1; } -struct OBSStudioAPI : obs_frontend_callbacks { - OBSBasic *main; - vector> callbacks; - vector> saveCallbacks; - vector> preloadCallbacks; +void *OBSStudioAPI::obs_frontend_get_main_window() +{ + return (void *)main; +} - inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} +void *OBSStudioAPI::obs_frontend_get_main_window_handle() +{ + return (void *)main->winId(); +} - void *obs_frontend_get_main_window(void) override { return (void *)main; } +void *OBSStudioAPI::obs_frontend_get_system_tray() +{ + return (void *)main->trayIcon.data(); +} - void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); } +void OBSStudioAPI::obs_frontend_get_scenes(struct obs_frontend_source_list *sources) +{ + for (int i = 0; i < main->ui->scenes->count(); i++) { + QListWidgetItem *item = main->ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + obs_source_t *source = obs_scene_get_source(scene); - void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); } - - void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->scenes->count(); i++) { - QListWidgetItem *item = main->ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - obs_source_t *source = obs_scene_get_source(scene); - - if (obs_source_get_ref(source) != nullptr) - da_push_back(sources->sources, &source); - } + if (obs_source_get_ref(source) != nullptr) + da_push_back(sources->sources, &source); } +} - obs_source_t *obs_frontend_get_current_scene(void) override - { - if (main->IsPreviewProgramMode()) { - return obs_weak_source_get_source(main->programScene); - } else { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } +obs_source_t *OBSStudioAPI::obs_frontend_get_current_scene() +{ + if (main->IsPreviewProgramMode()) { + return obs_weak_source_get_source(main->programScene); + } else { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); } +} - void obs_frontend_set_current_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene))); - } else { - QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); - } +void OBSStudioAPI::obs_frontend_set_current_scene(obs_source_t *scene) +{ + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene))); + } else { + QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), Q_ARG(OBSSource, OBSSource(scene)), + Q_ARG(bool, false)); } +} - void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->transitions->count(); i++) { - OBSSource tr = main->ui->transitions->itemData(i).value(); +void OBSStudioAPI::obs_frontend_get_transitions(struct obs_frontend_source_list *sources) +{ + for (int i = 0; i < main->ui->transitions->count(); i++) { + OBSSource tr = main->ui->transitions->itemData(i).value(); - if (!tr) - continue; + if (!tr) + continue; - if (obs_source_get_ref(tr) != nullptr) - da_push_back(sources->sources, &tr); - } + if (obs_source_get_ref(tr) != nullptr) + da_push_back(sources->sources, &tr); } +} - obs_source_t *obs_frontend_get_current_transition(void) override - { - OBSSource tr = main->GetCurrentTransition(); - return obs_source_get_ref(tr); +obs_source_t *OBSStudioAPI::obs_frontend_get_current_transition() +{ + OBSSource tr = main->GetCurrentTransition(); + return obs_source_get_ref(tr); +} + +void OBSStudioAPI::obs_frontend_set_current_transition(obs_source_t *transition) +{ + QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); +} + +int OBSStudioAPI::obs_frontend_get_transition_duration() +{ + return main->ui->transitionDuration->value(); +} + +void OBSStudioAPI::obs_frontend_set_transition_duration(int duration) +{ + QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); +} + +void OBSStudioAPI::obs_frontend_release_tbar() +{ + QMetaObject::invokeMethod(main, "TBarReleased"); +} + +void OBSStudioAPI::obs_frontend_set_tbar_position(int position) +{ + QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); +} + +int OBSStudioAPI::obs_frontend_get_tbar_position() +{ + return main->tBar->value(); +} + +void OBSStudioAPI::obs_frontend_get_scene_collections(std::vector &strings) +{ + for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { + strings.emplace_back(collectionName); } +} - void obs_frontend_set_current_transition(obs_source_t *transition) override - { - QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); - } +char *OBSStudioAPI::obs_frontend_get_current_scene_collection() +{ + const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); + return bstrdup(currentCollection.name.c_str()); +} - int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); } +void OBSStudioAPI::obs_frontend_set_current_scene_collection(const char *collection) +{ + QList menuActions = main->ui->sceneCollectionMenu->actions(); + QString qstrCollection = QT_UTF8(collection); - void obs_frontend_set_transition_duration(int duration) override - { - QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); - } + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); - void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); } - - void obs_frontend_set_tbar_position(int position) override - { - QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); - } - - int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); } - - void obs_frontend_get_scene_collections(std::vector &strings) override - { - for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { - strings.emplace_back(collectionName); - } - } - - char *obs_frontend_get_current_scene_collection(void) override - { - const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); - return bstrdup(currentCollection.name.c_str()); - } - - void obs_frontend_set_current_scene_collection(const char *collection) override - { - QList menuActions = main->ui->sceneCollectionMenu->actions(); - QString qstrCollection = QT_UTF8(collection); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrCollection) { - action->trigger(); - break; - } + if (v.typeName() != nullptr) { + if (action->text() == qstrCollection) { + action->trigger(); + break; } } } +} - bool obs_frontend_add_scene_collection(const char *name) override - { - bool success = false; - QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), - Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name))); - return success; +bool OBSStudioAPI::obs_frontend_add_scene_collection(const char *name) +{ + bool success = false; + QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), Q_RETURN_ARG(bool, success), + Q_ARG(QString, QT_UTF8(name))); + return success; +} + +void OBSStudioAPI::obs_frontend_get_profiles(std::vector &strings) +{ + const OBSProfileCache &profiles = main->GetProfileCache(); + + for (auto &[profileName, profile] : profiles) { + strings.emplace_back(profileName); } +} - void obs_frontend_get_profiles(std::vector &strings) override - { - const OBSProfileCache &profiles = main->GetProfileCache(); +char *OBSStudioAPI::obs_frontend_get_current_profile() +{ + const OBSProfile &profile = main->GetCurrentProfile(); + return bstrdup(profile.name.c_str()); +} - for (auto &[profileName, profile] : profiles) { - strings.emplace_back(profileName); - } - } +char *OBSStudioAPI::obs_frontend_get_current_profile_path() +{ + const OBSProfile &profile = main->GetCurrentProfile(); - char *obs_frontend_get_current_profile(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); - return bstrdup(profile.name.c_str()); - } + return bstrdup(profile.path.u8string().c_str()); +} - char *obs_frontend_get_current_profile_path(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); +void OBSStudioAPI::obs_frontend_set_current_profile(const char *profile) +{ + QList menuActions = main->ui->profileMenu->actions(); + QString qstrProfile = QT_UTF8(profile); - return bstrdup(profile.path.u8string().c_str()); - } + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); - void obs_frontend_set_current_profile(const char *profile) override - { - QList menuActions = main->ui->profileMenu->actions(); - QString qstrProfile = QT_UTF8(profile); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrProfile) { - action->trigger(); - break; - } + if (v.typeName() != nullptr) { + if (action->text() == qstrProfile) { + action->trigger(); + break; } } } +} - void obs_frontend_create_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); - } +void OBSStudioAPI::obs_frontend_create_profile(const char *name) +{ + QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); +} - void obs_frontend_duplicate_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); - } +void OBSStudioAPI::obs_frontend_duplicate_profile(const char *name) +{ + QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); +} - void obs_frontend_delete_profile(const char *profile) override - { - QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); - } +void OBSStudioAPI::obs_frontend_delete_profile(const char *profile) +{ + QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); +} - void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); } +void OBSStudioAPI::obs_frontend_streaming_start() +{ + QMetaObject::invokeMethod(main, "StartStreaming"); +} - void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); } +void OBSStudioAPI::obs_frontend_streaming_stop() +{ + QMetaObject::invokeMethod(main, "StopStreaming"); +} - bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); } +bool OBSStudioAPI::obs_frontend_streaming_active() +{ + return os_atomic_load_bool(&streaming_active); +} - void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); } +void OBSStudioAPI::obs_frontend_recording_start() +{ + QMetaObject::invokeMethod(main, "StartRecording"); +} - void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); } +void OBSStudioAPI::obs_frontend_recording_stop() +{ + QMetaObject::invokeMethod(main, "StopRecording"); +} - bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); } +bool OBSStudioAPI::obs_frontend_recording_active() +{ + return os_atomic_load_bool(&recording_active); +} - void obs_frontend_recording_pause(bool pause) override - { - QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); - } +void OBSStudioAPI::obs_frontend_recording_pause(bool pause) +{ + QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); +} - bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); } - - bool obs_frontend_recording_split_file(void) override - { - if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { - proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - uint8_t stack[128]; - calldata cd; - calldata_init_fixed(&cd, stack, sizeof(stack)); - proc_handler_call(ph, "split_file", &cd); - bool result = calldata_bool(&cd, "split_file_enabled"); - return result; - } else { - return false; - } - } - - bool obs_frontend_recording_add_chapter(const char *name) override - { - if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) - return false; +bool OBSStudioAPI::obs_frontend_recording_paused() +{ + return os_atomic_load_bool(&recording_paused); +} +bool OBSStudioAPI::obs_frontend_recording_split_file() +{ + if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - + uint8_t stack[128]; calldata cd; - calldata_init(&cd); - calldata_set_string(&cd, "chapter_name", name); - bool result = proc_handler_call(ph, "add_chapter", &cd); - calldata_free(&cd); + calldata_init_fixed(&cd, stack, sizeof(stack)); + proc_handler_call(ph, "split_file", &cd); + bool result = calldata_bool(&cd, "split_file_enabled"); return result; + } else { + return false; + } +} + +bool OBSStudioAPI::obs_frontend_recording_add_chapter(const char *name) +{ + if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) + return false; + + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + + calldata cd; + calldata_init(&cd); + calldata_set_string(&cd, "chapter_name", name); + bool result = proc_handler_call(ph, "add_chapter", &cd); + calldata_free(&cd); + return result; +} + +void OBSStudioAPI::obs_frontend_replay_buffer_start() +{ + QMetaObject::invokeMethod(main, "StartReplayBuffer"); +} + +void OBSStudioAPI::obs_frontend_replay_buffer_save() +{ + QMetaObject::invokeMethod(main, "ReplayBufferSave"); +} + +void OBSStudioAPI::obs_frontend_replay_buffer_stop() +{ + QMetaObject::invokeMethod(main, "StopReplayBuffer"); +} + +bool OBSStudioAPI::obs_frontend_replay_buffer_active() +{ + return os_atomic_load_bool(&replaybuf_active); +} + +void *OBSStudioAPI::obs_frontend_add_tools_menu_qaction(const char *name) +{ + main->ui->menuTools->setEnabled(true); + return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); +} + +void OBSStudioAPI::obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) +{ + main->ui->menuTools->setEnabled(true); + + auto func = [private_data, callback]() { + callback(private_data); + }; + + QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); + QObject::connect(action, &QAction::triggered, func); +} + +void *OBSStudioAPI::obs_frontend_add_dock(void *dock) +{ + QDockWidget *d = reinterpret_cast(dock); + + QString name = d->objectName(); + if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { + blog(LOG_WARNING, "The object name of the added dock is empty or already used," + " a temporary one will be set to avoid conflicts"); + + char *uuid = os_generate_uuid(); + name = QT_UTF8(uuid); + bfree(uuid); + name.append("_oldExtraDock"); + + d->setObjectName(name); } - void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); } + return (void *)main->AddDockWidget(d); +} - void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); } - - void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); } - - bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); } - - void *obs_frontend_add_tools_menu_qaction(const char *name) override - { - main->ui->menuTools->setEnabled(true); - return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); - } - - void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override - { - main->ui->menuTools->setEnabled(true); - - auto func = [private_data, callback]() { - callback(private_data); - }; - - QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); - QObject::connect(action, &QAction::triggered, func); - } - - void *obs_frontend_add_dock(void *dock) override - { - QDockWidget *d = reinterpret_cast(dock); - - QString name = d->objectName(); - if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { - blog(LOG_WARNING, "The object name of the added dock is empty or already used," - " a temporary one will be set to avoid conflicts"); - - char *uuid = os_generate_uuid(); - name = QT_UTF8(uuid); - bfree(uuid); - name.append("_oldExtraDock"); - - d->setObjectName(name); - } - - return (void *)main->AddDockWidget(d); - } - - bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - OBSDock *dock = new OBSDock(main); - dock->setWidget((QWidget *)widget); - dock->setWindowTitle(QT_UTF8(title)); - dock->setObjectName(QT_UTF8(id)); - - main->AddDockWidget(dock, Qt::RightDockWidgetArea); - - dock->setVisible(false); - dock->setFloating(true); - - return true; - } - - void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); } - - bool obs_frontend_add_custom_qdock(const char *id, void *dock) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - QDockWidget *d = reinterpret_cast(dock); - d->setObjectName(QT_UTF8(id)); - - main->AddCustomDockWidget(d); - - return true; - } - - void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - callbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - callbacks.erase(callbacks.begin() + idx); - } - - obs_output_t *obs_frontend_get_streaming_output(void) override - { - auto multitrackVideo = main->outputHandler->multitrackVideo.get(); - auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; - if (mtvOutput) - return mtvOutput; - - OBSOutput output = main->outputHandler->streamOutput.Get(); - return obs_output_get_ref(output); - } - - obs_output_t *obs_frontend_get_recording_output(void) override - { - OBSOutput out = main->outputHandler->fileOutput.Get(); - return obs_output_get_ref(out); - } - - obs_output_t *obs_frontend_get_replay_buffer_output(void) override - { - OBSOutput out = main->outputHandler->replayBuffer.Get(); - return obs_output_get_ref(out); - } - - config_t *obs_frontend_get_profile_config(void) override { return main->activeConfiguration; } - - config_t *obs_frontend_get_global_config(void) override - { +bool OBSStudioAPI::obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) +{ + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { blog(LOG_WARNING, - "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); - return App()->GetAppConfig(); + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; } - config_t *obs_frontend_get_app_config(void) override { return App()->GetAppConfig(); } + OBSDock *dock = new OBSDock(main); + dock->setWidget((QWidget *)widget); + dock->setWindowTitle(QT_UTF8(title)); + dock->setObjectName(QT_UTF8(id)); - config_t *obs_frontend_get_user_config(void) override { return App()->GetUserConfig(); } + main->AddDockWidget(dock, Qt::RightDockWidgetArea); - void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) override - { - SavedProjectorInfo proj = { - ProjectorType::Preview, - monitor, - geometry ? geometry : "", - name ? name : "", - }; - if (type) { - if (astrcmpi(type, "Source") == 0) - proj.type = ProjectorType::Source; - else if (astrcmpi(type, "Scene") == 0) - proj.type = ProjectorType::Scene; - else if (astrcmpi(type, "StudioProgram") == 0) - proj.type = ProjectorType::StudioProgram; - else if (astrcmpi(type, "Multiview") == 0) - proj.type = ProjectorType::Multiview; - } - QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), - Q_ARG(SavedProjectorInfo *, &proj)); + dock->setVisible(false); + dock->setFloating(true); + + return true; +} + +void OBSStudioAPI::obs_frontend_remove_dock(const char *id) +{ + main->RemoveDockWidget(QT_UTF8(id)); +} + +bool OBSStudioAPI::obs_frontend_add_custom_qdock(const char *id, void *dock) +{ + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; } - void obs_frontend_save(void) override { main->SaveProject(); } + QDockWidget *d = reinterpret_cast(dock); + d->setObjectName(QT_UTF8(id)); - void obs_frontend_defer_save_begin(void) override { QMetaObject::invokeMethod(main, "DeferSaveBegin"); } + main->AddCustomDockWidget(d); - void obs_frontend_defer_save_end(void) override { QMetaObject::invokeMethod(main, "DeferSaveEnd"); } + return true; +} - void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - saveCallbacks.emplace_back(callback, private_data); +void OBSStudioAPI::obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + callbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + callbacks.erase(callbacks.begin() + idx); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_streaming_output() +{ + auto multitrackVideo = main->outputHandler->multitrackVideo.get(); + auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; + if (mtvOutput) + return mtvOutput; + + OBSOutput output = main->outputHandler->streamOutput.Get(); + return obs_output_get_ref(output); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_recording_output() +{ + OBSOutput out = main->outputHandler->fileOutput.Get(); + return obs_output_get_ref(out); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_replay_buffer_output() +{ + OBSOutput out = main->outputHandler->replayBuffer.Get(); + return obs_output_get_ref(out); +} + +config_t *OBSStudioAPI::obs_frontend_get_profile_config() +{ + return main->activeConfiguration; +} + +config_t *OBSStudioAPI::obs_frontend_get_global_config() +{ + blog(LOG_WARNING, + "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); + return App()->GetAppConfig(); +} + +config_t *OBSStudioAPI::obs_frontend_get_app_config() +{ + return App()->GetAppConfig(); +} + +config_t *OBSStudioAPI::obs_frontend_get_user_config() +{ + return App()->GetUserConfig(); +} + +void OBSStudioAPI::obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) +{ + SavedProjectorInfo proj = { + ProjectorType::Preview, + monitor, + geometry ? geometry : "", + name ? name : "", + }; + if (type) { + if (astrcmpi(type, "Source") == 0) + proj.type = ProjectorType::Source; + else if (astrcmpi(type, "Scene") == 0) + proj.type = ProjectorType::Scene; + else if (astrcmpi(type, "StudioProgram") == 0) + proj.type = ProjectorType::StudioProgram; + else if (astrcmpi(type, "Multiview") == 0) + proj.type = ProjectorType::Multiview; + } + QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), Q_ARG(SavedProjectorInfo *, &proj)); +} + +void OBSStudioAPI::obs_frontend_save() +{ + main->SaveProject(); +} + +void OBSStudioAPI::obs_frontend_defer_save_begin() +{ + QMetaObject::invokeMethod(main, "DeferSaveBegin"); +} + +void OBSStudioAPI::obs_frontend_defer_save_end() +{ + QMetaObject::invokeMethod(main, "DeferSaveEnd"); +} + +void OBSStudioAPI::obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + saveCallbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + saveCallbacks.erase(saveCallbacks.begin() + idx); +} + +void OBSStudioAPI::obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + preloadCallbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + preloadCallbacks.erase(preloadCallbacks.begin() + idx); +} + +void OBSStudioAPI::obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) +{ + App()->PushUITranslation(translate); +} + +void OBSStudioAPI::obs_frontend_pop_ui_translation() +{ + App()->PopUITranslation(); +} + +void OBSStudioAPI::obs_frontend_set_streaming_service(obs_service_t *service) +{ + main->SetService(service); +} + +obs_service_t *OBSStudioAPI::obs_frontend_get_streaming_service() +{ + return main->GetService(); +} + +void OBSStudioAPI::obs_frontend_save_streaming_service() +{ + main->SaveService(); +} + +bool OBSStudioAPI::obs_frontend_preview_program_mode_active() +{ + return main->IsPreviewProgramMode(); +} + +void OBSStudioAPI::obs_frontend_set_preview_program_mode(bool enable) +{ + main->SetPreviewProgramMode(enable); +} + +void OBSStudioAPI::obs_frontend_preview_program_trigger_transition() +{ + QMetaObject::invokeMethod(main, "TransitionClicked"); +} + +bool OBSStudioAPI::obs_frontend_preview_enabled() +{ + return main->previewEnabled; +} + +void OBSStudioAPI::obs_frontend_set_preview_enabled(bool enable) +{ + if (main->previewEnabled != enable) + main->EnablePreviewDisplay(enable); +} + +obs_source_t *OBSStudioAPI::obs_frontend_get_current_preview_scene() +{ + if (main->IsPreviewProgramMode()) { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); } - void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; + return nullptr; +} - saveCallbacks.erase(saveCallbacks.begin() + idx); +void OBSStudioAPI::obs_frontend_set_current_preview_scene(obs_source_t *scene) +{ + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), + Q_ARG(bool, false)); } +} - void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - preloadCallbacks.emplace_back(callback, private_data); +void OBSStudioAPI::obs_frontend_take_screenshot() +{ + QMetaObject::invokeMethod(main, "Screenshot"); +} + +void OBSStudioAPI::obs_frontend_take_source_screenshot(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_virtualcam_output() +{ + OBSOutput output = main->outputHandler->virtualCam.Get(); + return obs_output_get_ref(output); +} + +void OBSStudioAPI::obs_frontend_start_virtualcam() +{ + QMetaObject::invokeMethod(main, "StartVirtualCam"); +} + +void OBSStudioAPI::obs_frontend_stop_virtualcam() +{ + QMetaObject::invokeMethod(main, "StopVirtualCam"); +} + +bool OBSStudioAPI::obs_frontend_virtualcam_active() +{ + return os_atomic_load_bool(&virtualcam_active); +} + +void OBSStudioAPI::obs_frontend_reset_video() +{ + main->ResetVideo(); +} + +void OBSStudioAPI::obs_frontend_open_source_properties(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_source_filters(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_source_interaction(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) +{ + QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +char *OBSStudioAPI::obs_frontend_get_current_record_output_path() +{ + const char *recordOutputPath = main->GetCurrentOutputPath(); + + return bstrdup(recordOutputPath); +} + +const char *OBSStudioAPI::obs_frontend_get_locale_string(const char *string) +{ + return Str(string); +} + +bool OBSStudioAPI::obs_frontend_is_theme_dark() +{ + return App()->IsThemeDark(); +} + +char *OBSStudioAPI::obs_frontend_get_last_recording() +{ + return bstrdup(main->outputHandler->lastRecordingPath.c_str()); +} + +char *OBSStudioAPI::obs_frontend_get_last_screenshot() +{ + return bstrdup(main->lastScreenshot.c_str()); +} + +char *OBSStudioAPI::obs_frontend_get_last_replay() +{ + return bstrdup(main->lastReplay.c_str()); +} + +void OBSStudioAPI::obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, + const char *undo_data, const char *redo_data, bool repeatable) +{ + main->undo_s.add_action( + name, [undo](const std::string &data) { undo(data.c_str()); }, + [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); +} + +void OBSStudioAPI::on_load(obs_data_t *settings) +{ + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); } +} - void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - preloadCallbacks.erase(preloadCallbacks.begin() + idx); +void OBSStudioAPI::on_preload(obs_data_t *settings) +{ + for (size_t i = preloadCallbacks.size(); i > 0; i--) { + auto cb = preloadCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); } +} - void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override - { - App()->PushUITranslation(translate); +void OBSStudioAPI::on_save(obs_data_t *settings) +{ + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, true, cb.private_data); } +} - void obs_frontend_pop_ui_translation(void) override { App()->PopUITranslation(); } +void OBSStudioAPI::on_event(enum obs_frontend_event event) +{ + if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && + event != OBS_FRONTEND_EVENT_EXIT) + return; - void obs_frontend_set_streaming_service(obs_service_t *service) override { main->SetService(service); } - - obs_service_t *obs_frontend_get_streaming_service(void) override { return main->GetService(); } - - void obs_frontend_save_streaming_service(void) override { main->SaveService(); } - - bool obs_frontend_preview_program_mode_active(void) override { return main->IsPreviewProgramMode(); } - - void obs_frontend_set_preview_program_mode(bool enable) override { main->SetPreviewProgramMode(enable); } - - void obs_frontend_preview_program_trigger_transition(void) override - { - QMetaObject::invokeMethod(main, "TransitionClicked"); + for (size_t i = callbacks.size(); i > 0; i--) { + auto cb = callbacks[i - 1]; + cb.callback(event, cb.private_data); } - - bool obs_frontend_preview_enabled(void) override { return main->previewEnabled; } - - void obs_frontend_set_preview_enabled(bool enable) override - { - if (main->previewEnabled != enable) - main->EnablePreviewDisplay(enable); - } - - obs_source_t *obs_frontend_get_current_preview_scene(void) override - { - if (main->IsPreviewProgramMode()) { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - - return nullptr; - } - - void obs_frontend_set_current_preview_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), - Q_ARG(bool, false)); - } - } - - void obs_frontend_take_screenshot(void) override { QMetaObject::invokeMethod(main, "Screenshot"); } - - void obs_frontend_take_source_screenshot(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); - } - - obs_output_t *obs_frontend_get_virtualcam_output(void) override - { - OBSOutput output = main->outputHandler->virtualCam.Get(); - return obs_output_get_ref(output); - } - - void obs_frontend_start_virtualcam(void) override { QMetaObject::invokeMethod(main, "StartVirtualCam"); } - - void obs_frontend_stop_virtualcam(void) override { QMetaObject::invokeMethod(main, "StopVirtualCam"); } - - bool obs_frontend_virtualcam_active(void) override { return os_atomic_load_bool(&virtualcam_active); } - - void obs_frontend_reset_video(void) override { main->ResetVideo(); } - - void obs_frontend_open_source_properties(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_source_filters(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_source_interaction(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override - { - QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); - } - - char *obs_frontend_get_current_record_output_path(void) override - { - const char *recordOutputPath = main->GetCurrentOutputPath(); - - return bstrdup(recordOutputPath); - } - - const char *obs_frontend_get_locale_string(const char *string) override { return Str(string); } - - bool obs_frontend_is_theme_dark(void) override { return App()->IsThemeDark(); } - - char *obs_frontend_get_last_recording(void) override - { - return bstrdup(main->outputHandler->lastRecordingPath.c_str()); - } - - char *obs_frontend_get_last_screenshot(void) override { return bstrdup(main->lastScreenshot.c_str()); } - - char *obs_frontend_get_last_replay(void) override { return bstrdup(main->lastReplay.c_str()); } - - void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, - const char *undo_data, const char *redo_data, bool repeatable) override - { - main->undo_s.add_action( - name, [undo](const std::string &data) { undo(data.c_str()); }, - [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); - } - - void on_load(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_preload(obs_data_t *settings) override - { - for (size_t i = preloadCallbacks.size(); i > 0; i--) { - auto cb = preloadCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_save(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, true, cb.private_data); - } - } - - void on_event(enum obs_frontend_event event) override - { - if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && - event != OBS_FRONTEND_EVENT_EXIT) - return; - - for (size_t i = callbacks.size(); i > 0; i--) { - auto cb = callbacks[i - 1]; - cb.callback(event, cb.private_data); - } - } -}; +} obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) { diff --git a/frontend/OBSStudioAPI.hpp b/frontend/OBSStudioAPI.hpp index bb3715017..5a1872d85 100644 --- a/frontend/OBSStudioAPI.hpp +++ b/frontend/OBSStudioAPI.hpp @@ -1,29 +1,28 @@ -#include -#include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer -#include + 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 + +class OBSBasic; using namespace std; -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -extern volatile bool streaming_active; -extern volatile bool recording_active; -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; -extern volatile bool virtualcam_active; - -/* ------------------------------------------------------------------------- */ - template struct OBSStudioCallback { T callback; void *private_data; @@ -31,18 +30,6 @@ template struct OBSStudioCallback { inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} }; -template -inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) -{ - for (size_t i = 0; i < callbacks.size(); i++) { - OBSStudioCallback curCB = callbacks[i]; - if (curCB.callback == callback && curCB.private_data == private_data) - return i; - } - - return (size_t)-1; -} - struct OBSStudioAPI : obs_frontend_callbacks { OBSBasic *main; vector> callbacks; @@ -51,586 +38,199 @@ struct OBSStudioAPI : obs_frontend_callbacks { inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} - void *obs_frontend_get_main_window(void) override { return (void *)main; } - - void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); } - - void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); } - - void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->scenes->count(); i++) { - QListWidgetItem *item = main->ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - obs_source_t *source = obs_scene_get_source(scene); - - if (obs_source_get_ref(source) != nullptr) - da_push_back(sources->sources, &source); - } - } - - obs_source_t *obs_frontend_get_current_scene(void) override - { - if (main->IsPreviewProgramMode()) { - return obs_weak_source_get_source(main->programScene); - } else { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - } - - void obs_frontend_set_current_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene))); - } else { - QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); - } - } - - void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->transitions->count(); i++) { - OBSSource tr = main->ui->transitions->itemData(i).value(); - - if (!tr) - continue; - - if (obs_source_get_ref(tr) != nullptr) - da_push_back(sources->sources, &tr); - } - } - - obs_source_t *obs_frontend_get_current_transition(void) override - { - OBSSource tr = main->GetCurrentTransition(); - return obs_source_get_ref(tr); - } - - void obs_frontend_set_current_transition(obs_source_t *transition) override - { - QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); - } - - int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); } - - void obs_frontend_set_transition_duration(int duration) override - { - QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); - } - - void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); } - - void obs_frontend_set_tbar_position(int position) override - { - QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); - } - - int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); } - - void obs_frontend_get_scene_collections(std::vector &strings) override - { - for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { - strings.emplace_back(collectionName); - } - } - - char *obs_frontend_get_current_scene_collection(void) override - { - const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); - return bstrdup(currentCollection.name.c_str()); - } - - void obs_frontend_set_current_scene_collection(const char *collection) override - { - QList menuActions = main->ui->sceneCollectionMenu->actions(); - QString qstrCollection = QT_UTF8(collection); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrCollection) { - action->trigger(); - break; - } - } - } - } - - bool obs_frontend_add_scene_collection(const char *name) override - { - bool success = false; - QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), - Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name))); - return success; - } - - void obs_frontend_get_profiles(std::vector &strings) override - { - const OBSProfileCache &profiles = main->GetProfileCache(); - - for (auto &[profileName, profile] : profiles) { - strings.emplace_back(profileName); - } - } - - char *obs_frontend_get_current_profile(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); - return bstrdup(profile.name.c_str()); - } - - char *obs_frontend_get_current_profile_path(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); - - return bstrdup(profile.path.u8string().c_str()); - } - - void obs_frontend_set_current_profile(const char *profile) override - { - QList menuActions = main->ui->profileMenu->actions(); - QString qstrProfile = QT_UTF8(profile); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrProfile) { - action->trigger(); - break; - } - } - } - } - - void obs_frontend_create_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); - } - - void obs_frontend_duplicate_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); - } - - void obs_frontend_delete_profile(const char *profile) override - { - QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); - } - - void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); } - - void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); } - - bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); } - - void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); } - - void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); } - - bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); } - - void obs_frontend_recording_pause(bool pause) override - { - QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); - } - - bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); } - - bool obs_frontend_recording_split_file(void) override - { - if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { - proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - uint8_t stack[128]; - calldata cd; - calldata_init_fixed(&cd, stack, sizeof(stack)); - proc_handler_call(ph, "split_file", &cd); - bool result = calldata_bool(&cd, "split_file_enabled"); - return result; - } else { - return false; - } - } - - bool obs_frontend_recording_add_chapter(const char *name) override - { - if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) - return false; - - proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - - calldata cd; - calldata_init(&cd); - calldata_set_string(&cd, "chapter_name", name); - bool result = proc_handler_call(ph, "add_chapter", &cd); - calldata_free(&cd); - return result; - } - - void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); } - - void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); } - - void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); } - - bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); } - - void *obs_frontend_add_tools_menu_qaction(const char *name) override - { - main->ui->menuTools->setEnabled(true); - return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); - } - - void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override - { - main->ui->menuTools->setEnabled(true); - - auto func = [private_data, callback]() { - callback(private_data); - }; - - QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); - QObject::connect(action, &QAction::triggered, func); - } - - void *obs_frontend_add_dock(void *dock) override - { - QDockWidget *d = reinterpret_cast(dock); - - QString name = d->objectName(); - if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { - blog(LOG_WARNING, "The object name of the added dock is empty or already used," - " a temporary one will be set to avoid conflicts"); - - char *uuid = os_generate_uuid(); - name = QT_UTF8(uuid); - bfree(uuid); - name.append("_oldExtraDock"); - - d->setObjectName(name); - } - - return (void *)main->AddDockWidget(d); - } - - bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - OBSDock *dock = new OBSDock(main); - dock->setWidget((QWidget *)widget); - dock->setWindowTitle(QT_UTF8(title)); - dock->setObjectName(QT_UTF8(id)); - - main->AddDockWidget(dock, Qt::RightDockWidgetArea); - - dock->setVisible(false); - dock->setFloating(true); - - return true; - } - - void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); } - - bool obs_frontend_add_custom_qdock(const char *id, void *dock) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - QDockWidget *d = reinterpret_cast(dock); - d->setObjectName(QT_UTF8(id)); - - main->AddCustomDockWidget(d); - - return true; - } - - void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - callbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - callbacks.erase(callbacks.begin() + idx); - } - - obs_output_t *obs_frontend_get_streaming_output(void) override - { - auto multitrackVideo = main->outputHandler->multitrackVideo.get(); - auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; - if (mtvOutput) - return mtvOutput; - - OBSOutput output = main->outputHandler->streamOutput.Get(); - return obs_output_get_ref(output); - } - - obs_output_t *obs_frontend_get_recording_output(void) override - { - OBSOutput out = main->outputHandler->fileOutput.Get(); - return obs_output_get_ref(out); - } - - obs_output_t *obs_frontend_get_replay_buffer_output(void) override - { - OBSOutput out = main->outputHandler->replayBuffer.Get(); - return obs_output_get_ref(out); - } - - config_t *obs_frontend_get_profile_config(void) override { return main->activeConfiguration; } - - config_t *obs_frontend_get_global_config(void) override - { - blog(LOG_WARNING, - "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); - return App()->GetAppConfig(); - } - - config_t *obs_frontend_get_app_config(void) override { return App()->GetAppConfig(); } - - config_t *obs_frontend_get_user_config(void) override { return App()->GetUserConfig(); } - - void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) override - { - SavedProjectorInfo proj = { - ProjectorType::Preview, - monitor, - geometry ? geometry : "", - name ? name : "", - }; - if (type) { - if (astrcmpi(type, "Source") == 0) - proj.type = ProjectorType::Source; - else if (astrcmpi(type, "Scene") == 0) - proj.type = ProjectorType::Scene; - else if (astrcmpi(type, "StudioProgram") == 0) - proj.type = ProjectorType::StudioProgram; - else if (astrcmpi(type, "Multiview") == 0) - proj.type = ProjectorType::Multiview; - } - QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), - Q_ARG(SavedProjectorInfo *, &proj)); - } - - void obs_frontend_save(void) override { main->SaveProject(); } - - void obs_frontend_defer_save_begin(void) override { QMetaObject::invokeMethod(main, "DeferSaveBegin"); } - - void obs_frontend_defer_save_end(void) override { QMetaObject::invokeMethod(main, "DeferSaveEnd"); } - - void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - saveCallbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - saveCallbacks.erase(saveCallbacks.begin() + idx); - } - - void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - preloadCallbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - preloadCallbacks.erase(preloadCallbacks.begin() + idx); - } - - void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override - { - App()->PushUITranslation(translate); - } - - void obs_frontend_pop_ui_translation(void) override { App()->PopUITranslation(); } - - void obs_frontend_set_streaming_service(obs_service_t *service) override { main->SetService(service); } - - obs_service_t *obs_frontend_get_streaming_service(void) override { return main->GetService(); } - - void obs_frontend_save_streaming_service(void) override { main->SaveService(); } - - bool obs_frontend_preview_program_mode_active(void) override { return main->IsPreviewProgramMode(); } - - void obs_frontend_set_preview_program_mode(bool enable) override { main->SetPreviewProgramMode(enable); } - - void obs_frontend_preview_program_trigger_transition(void) override - { - QMetaObject::invokeMethod(main, "TransitionClicked"); - } - - bool obs_frontend_preview_enabled(void) override { return main->previewEnabled; } - - void obs_frontend_set_preview_enabled(bool enable) override - { - if (main->previewEnabled != enable) - main->EnablePreviewDisplay(enable); - } - - obs_source_t *obs_frontend_get_current_preview_scene(void) override - { - if (main->IsPreviewProgramMode()) { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - - return nullptr; - } - - void obs_frontend_set_current_preview_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), - Q_ARG(bool, false)); - } - } + void *obs_frontend_get_main_window(void) override; - void obs_frontend_take_screenshot(void) override { QMetaObject::invokeMethod(main, "Screenshot"); } + void *obs_frontend_get_main_window_handle(void) override; - void obs_frontend_take_source_screenshot(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); - } + void *obs_frontend_get_system_tray(void) override; - obs_output_t *obs_frontend_get_virtualcam_output(void) override - { - OBSOutput output = main->outputHandler->virtualCam.Get(); - return obs_output_get_ref(output); - } + void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override; - void obs_frontend_start_virtualcam(void) override { QMetaObject::invokeMethod(main, "StartVirtualCam"); } + obs_source_t *obs_frontend_get_current_scene(void) override; - void obs_frontend_stop_virtualcam(void) override { QMetaObject::invokeMethod(main, "StopVirtualCam"); } + void obs_frontend_set_current_scene(obs_source_t *scene) override; - bool obs_frontend_virtualcam_active(void) override { return os_atomic_load_bool(&virtualcam_active); } + void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override; - void obs_frontend_reset_video(void) override { main->ResetVideo(); } + obs_source_t *obs_frontend_get_current_transition(void) override; - void obs_frontend_open_source_properties(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); - } + void obs_frontend_set_current_transition(obs_source_t *transition) override; - void obs_frontend_open_source_filters(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); - } + int obs_frontend_get_transition_duration(void) override; - void obs_frontend_open_source_interaction(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); - } + void obs_frontend_set_transition_duration(int duration) override; - void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override - { - QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); - } + void obs_frontend_release_tbar(void) override; - char *obs_frontend_get_current_record_output_path(void) override - { - const char *recordOutputPath = main->GetCurrentOutputPath(); + void obs_frontend_set_tbar_position(int position) override; - return bstrdup(recordOutputPath); - } + int obs_frontend_get_tbar_position(void) override; - const char *obs_frontend_get_locale_string(const char *string) override { return Str(string); } + void obs_frontend_get_scene_collections(std::vector &strings) override; - bool obs_frontend_is_theme_dark(void) override { return App()->IsThemeDark(); } + char *obs_frontend_get_current_scene_collection(void) override; - char *obs_frontend_get_last_recording(void) override - { - return bstrdup(main->outputHandler->lastRecordingPath.c_str()); - } + void obs_frontend_set_current_scene_collection(const char *collection) override; - char *obs_frontend_get_last_screenshot(void) override { return bstrdup(main->lastScreenshot.c_str()); } + bool obs_frontend_add_scene_collection(const char *name) override; - char *obs_frontend_get_last_replay(void) override { return bstrdup(main->lastReplay.c_str()); } + void obs_frontend_get_profiles(std::vector &strings) override; + + char *obs_frontend_get_current_profile(void) override; + + char *obs_frontend_get_current_profile_path(void) override; + + void obs_frontend_set_current_profile(const char *profile) override; + + void obs_frontend_create_profile(const char *name) override; + + void obs_frontend_duplicate_profile(const char *name) override; + + void obs_frontend_delete_profile(const char *profile) override; + + void obs_frontend_streaming_start(void) override; + + void obs_frontend_streaming_stop(void) override; + + bool obs_frontend_streaming_active(void) override; + + void obs_frontend_recording_start(void) override; + + void obs_frontend_recording_stop(void) override; + + bool obs_frontend_recording_active(void) override; + + void obs_frontend_recording_pause(bool pause) override; + + bool obs_frontend_recording_paused(void) override; + + bool obs_frontend_recording_split_file(void) override; + + bool obs_frontend_recording_add_chapter(const char *name) override; + + void obs_frontend_replay_buffer_start(void) override; + + void obs_frontend_replay_buffer_save(void) override; + + void obs_frontend_replay_buffer_stop(void) override; + + bool obs_frontend_replay_buffer_active(void) override; + + void *obs_frontend_add_tools_menu_qaction(const char *name) override; + + void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override; + + void *obs_frontend_add_dock(void *dock) override; + + bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override; + + void obs_frontend_remove_dock(const char *id) override; + + bool obs_frontend_add_custom_qdock(const char *id, void *dock) override; + + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override; + + void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override; + + obs_output_t *obs_frontend_get_streaming_output(void) override; + + obs_output_t *obs_frontend_get_recording_output(void) override; + + obs_output_t *obs_frontend_get_replay_buffer_output(void) override; + + config_t *obs_frontend_get_profile_config(void) override; + + config_t *obs_frontend_get_global_config(void) override; + + config_t *obs_frontend_get_app_config(void) override; + + config_t *obs_frontend_get_user_config(void) override; + + void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, + const char *name) override; + + void obs_frontend_save(void) override; + + void obs_frontend_defer_save_begin(void) override; + + void obs_frontend_defer_save_end(void) override; + + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override; + + void obs_frontend_pop_ui_translation(void) override; + + void obs_frontend_set_streaming_service(obs_service_t *service) override; + + obs_service_t *obs_frontend_get_streaming_service(void) override; + + void obs_frontend_save_streaming_service(void) override; + + bool obs_frontend_preview_program_mode_active(void) override; + + void obs_frontend_set_preview_program_mode(bool enable) override; + + void obs_frontend_preview_program_trigger_transition(void) override; + + bool obs_frontend_preview_enabled(void) override; + + void obs_frontend_set_preview_enabled(bool enable) override; + + obs_source_t *obs_frontend_get_current_preview_scene(void) override; + + void obs_frontend_set_current_preview_scene(obs_source_t *scene) override; + + void obs_frontend_take_screenshot(void) override; + + void obs_frontend_take_source_screenshot(obs_source_t *source) override; + + obs_output_t *obs_frontend_get_virtualcam_output(void) override; + + void obs_frontend_start_virtualcam(void) override; + + void obs_frontend_stop_virtualcam(void) override; + + bool obs_frontend_virtualcam_active(void) override; + + void obs_frontend_reset_video(void) override; + + void obs_frontend_open_source_properties(obs_source_t *source) override; + + void obs_frontend_open_source_filters(obs_source_t *source) override; + + void obs_frontend_open_source_interaction(obs_source_t *source) override; + + void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override; + + char *obs_frontend_get_current_record_output_path(void) override; + + const char *obs_frontend_get_locale_string(const char *string) override; + + bool obs_frontend_is_theme_dark(void) override; + + char *obs_frontend_get_last_recording(void) override; + + char *obs_frontend_get_last_screenshot(void) override; + + char *obs_frontend_get_last_replay(void) override; void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, - const char *undo_data, const char *redo_data, bool repeatable) override - { - main->undo_s.add_action( - name, [undo](const std::string &data) { undo(data.c_str()); }, - [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); - } + const char *undo_data, const char *redo_data, bool repeatable) override; - void on_load(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } + void on_load(obs_data_t *settings) override; - void on_preload(obs_data_t *settings) override - { - for (size_t i = preloadCallbacks.size(); i > 0; i--) { - auto cb = preloadCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } + void on_preload(obs_data_t *settings) override; - void on_save(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, true, cb.private_data); - } - } + void on_save(obs_data_t *settings) override; - void on_event(enum obs_frontend_event event) override - { - if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && - event != OBS_FRONTEND_EVENT_EXIT) - return; - - for (size_t i = callbacks.size(); i > 0; i--) { - auto cb = callbacks[i - 1]; - cb.callback(event, cb.private_data); - } - } + void on_event(enum obs_frontend_event event) override; }; -obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) -{ - obs_frontend_callbacks *api = new OBSStudioAPI(main); - obs_frontend_set_callbacks_internal(api); - return api; -} +obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp index 883bee21a..987dca5bc 100644 --- a/frontend/obs-main.cpp +++ b/frontend/obs-main.cpp @@ -15,86 +15,52 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "obs-proxy-style.hpp" -#include "log-viewer.hpp" -#include "volume-control.hpp" -#include "window-basic-main.hpp" +#include +#include #ifdef __APPLE__ -#include "window-permissions.hpp" +#include #endif -#include "window-basic-settings.hpp" -#include "platform.hpp" +#include +#include +#include +#include -#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif +#include #include +#include +#include +#include #ifdef _WIN32 +#include +#define WIN32_LEAN_AND_MEAN #include -#include -#include #else #include -#include -#include -#include -#include #endif -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -#include "update/models/branches.hpp" -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) -#include -#include -#endif - -#include - -#include "ui-config.h" - using namespace std; static log_handler_t def_log_handler; -static string currentLogFile; -static string lastLogFile; -static string lastCrashLogFile; +extern string currentLogFile; +extern string lastLogFile; +extern string lastCrashLogFile; bool portable_mode = false; bool steam = false; bool safe_mode = false; bool disable_3p_plugins = false; -bool unclean_shutdown = false; -bool disable_shutdown_check = false; +static bool unclean_shutdown = false; +static bool disable_shutdown_check = false; static bool multi = false; static bool log_verbose = false; static bool unfiltered_log = false; @@ -114,144 +80,10 @@ string opt_starting_scene; bool restart = false; bool restart_safe = false; -QStringList arguments; +static QStringList arguments; QPointer obsLogViewer; -#ifndef _WIN32 -int OBSApp::sigintFd[2]; -#endif - -// GPU hint exports for AMD/NVIDIA laptops -#ifdef _MSC_VER -extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; -extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; -#endif - -QObject *CreateShortcutFilter() -{ - return new OBSEventFilter([](QObject *obj, QEvent *event) { - auto mouse_event = [](QMouseEvent &event) { - if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) - return true; - - obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; - bool pressed = event.type() == QEvent::MouseButtonPress; - - switch (event.button()) { - case Qt::NoButton: - case Qt::LeftButton: - case Qt::RightButton: - case Qt::AllButtons: - case Qt::MouseButtonMask: - return false; - - case Qt::MiddleButton: - hotkey.key = OBS_KEY_MOUSE3; - break; - -#define MAP_BUTTON(i, j) \ - case Qt::ExtraButton##i: \ - hotkey.key = OBS_KEY_MOUSE##j; \ - break; - MAP_BUTTON(1, 4); - MAP_BUTTON(2, 5); - MAP_BUTTON(3, 6); - MAP_BUTTON(4, 7); - MAP_BUTTON(5, 8); - MAP_BUTTON(6, 9); - MAP_BUTTON(7, 10); - MAP_BUTTON(8, 11); - MAP_BUTTON(9, 12); - MAP_BUTTON(10, 13); - MAP_BUTTON(11, 14); - MAP_BUTTON(12, 15); - MAP_BUTTON(13, 16); - MAP_BUTTON(14, 17); - MAP_BUTTON(15, 18); - MAP_BUTTON(16, 19); - MAP_BUTTON(17, 20); - MAP_BUTTON(18, 21); - MAP_BUTTON(19, 22); - MAP_BUTTON(20, 23); - MAP_BUTTON(21, 24); - MAP_BUTTON(22, 25); - MAP_BUTTON(23, 26); - MAP_BUTTON(24, 27); -#undef MAP_BUTTON - } - - hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); - - obs_hotkey_inject_event(hotkey, pressed); - return true; - }; - - auto key_event = [&](QKeyEvent *event) { - int key = event->key(); - bool enabledInFocus = App()->HotkeysEnabledInFocus(); - - if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) - return true; - - QDialog *dialog = qobject_cast(obj); - - obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; - bool pressed = event->type() == QEvent::KeyPress; - - switch (key) { - case Qt::Key_Shift: - case Qt::Key_Control: - case Qt::Key_Alt: - case Qt::Key_Meta: - break; - -#ifdef __APPLE__ - case Qt::Key_CapsLock: - // kVK_CapsLock == 57 - hotkey.key = obs_key_from_virtual_key(57); - pressed = true; - break; -#endif - - case Qt::Key_Enter: - case Qt::Key_Escape: - case Qt::Key_Return: - if (dialog && pressed) - return false; - if (!enabledInFocus) - return true; - /* Falls through. */ - default: - hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); - } - - if (event->isAutoRepeat()) - return true; - - hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - obs_hotkey_inject_event(hotkey, pressed); - return true; - }; - - switch (event->type()) { - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - return mouse_event(*static_cast(event)); - - /*case QEvent::MouseButtonDblClick: - case QEvent::Wheel:*/ - case QEvent::KeyPress: - case QEvent::KeyRelease: - return key_event(static_cast(event)); - - default: - return false; - } - }); -} - string CurrentTimeString() { using namespace std::chrono; @@ -274,16 +106,6 @@ string CurrentTimeString() return buf; } -string CurrentDateTimeString() -{ - time_t now = time(0); - struct tm tstruct; - char buf[80]; - tstruct = *localtime(&now); - strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); - return buf; -} - static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) { static mutex logfile_mutex; @@ -423,1031 +245,6 @@ static void do_log(int log_level, const char *msg, va_list args, void *param) #endif } -#define DEFAULT_LANG "en-US" - -bool OBSApp::InitGlobalConfigDefaults() -{ - config_set_default_uint(appConfig, "General", "MaxLogs", 10); - config_set_default_int(appConfig, "General", "InfoIncrement", -1); - config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); - config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); - -#if _WIN32 - config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); -#else - config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); -#endif - -#ifdef _WIN32 - config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); - config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); -#endif - -#ifdef __APPLE__ - config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); - config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); - config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); -#endif - - return true; -} - -bool OBSApp::InitGlobalLocationDefaults() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), nullptr); - if (len <= 0) { - OBSErrorBox(NULL, "Unable to get global configuration path."); - return false; - } - - config_set_default_string(appConfig, "Locations", "Configuration", path); - config_set_default_string(appConfig, "Locations", "SceneCollections", path); - config_set_default_string(appConfig, "Locations", "Profiles", path); - - return true; -} - -void OBSApp::InitUserConfigDefaults() -{ - config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); - - config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); - - config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); - config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); - config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); - config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); - config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); - config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); - config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); - config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); - config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); - config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); - config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); - config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); - config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); - - config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); - - config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); -} - -static bool do_mkdir(const char *path) -{ - if (os_mkdirs(path) == MKDIR_ERROR) { - OBSErrorBox(NULL, "Failed to create directory %s", path); - return false; - } - - return true; -} - -static bool MakeUserDirs() -{ - char path[512]; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) - return false; - if (!do_mkdir(path)) - return false; - -#ifdef _WIN32 - if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) - return false; - if (!do_mkdir(path)) - return false; -#endif - -#ifdef WHATSNEW_ENABLED - if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) - return false; - if (!do_mkdir(path)) - return false; -#endif - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - return true; -} - -constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; -constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; - -static bool MakeUserProfileDirs() -{ - const std::filesystem::path userProfilePath = - App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); - const std::filesystem::path userScenesPath = - App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); - - if (!std::filesystem::exists(userProfilePath)) { - try { - std::filesystem::create_directories(userProfilePath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create user profile directory '%s'\n%s", - userProfilePath.u8string().c_str(), error.what()); - return false; - } - } - - if (!std::filesystem::exists(userScenesPath)) { - try { - std::filesystem::create_directories(userScenesPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create user scene collection directory '%s'\n%s", - userScenesPath.u8string().c_str(), error.what()); - return false; - } - } - - return true; -} - -bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) -{ - if (!layout) - return false; - - if (astrcmpi(layout, "horizontaltop") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "horizontalbottom") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "verticalleft") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "verticalright") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); - return true; - } - - return false; -} - -bool OBSApp::InitGlobalConfig() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); - if (len <= 0) { - return false; - } - - int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); - if (errorcode != CONFIG_SUCCESS) { - OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); - return false; - } - - uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); - - if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { - bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); - - if (!migratedUserSettings) { - bool migrated = MigrateGlobalSettings(); - - config_set_bool(appConfig, "General", "Pre31Migrated", migrated); - config_save_safe(appConfig, "tmp", nullptr); - } - } - - InitGlobalConfigDefaults(); - InitGlobalLocationDefaults(); - - if (IsPortableMode()) { - userConfigLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); - userScenesLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); - userProfilesLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); - } else { - userConfigLocation = - std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); - userScenesLocation = - std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); - userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); - } - - bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); - - return userConfigResult; -} - -bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) -{ - const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; - - int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); - - if (errorCode != CONFIG_SUCCESS) { - OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); - return false; - } - - MigrateLegacySettings(lastVersion); - InitUserConfigDefaults(); - - return true; -} - -void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) -{ - bool hasChanges = false; - - const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); - const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); - const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); - const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); - const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); - - const map defaultsMap{ - {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; - - for (auto &[version, configKey] : defaultsMap) { - if (!config_has_user_value(userConfig, "General", configKey.c_str())) { - bool useOldDefaults = lastVersion && lastVersion < version; - config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); - - hasChanges = true; - } - } - - if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { - const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); - - bool layoutUpdated = UpdatePre22MultiviewLayout(layout); - - hasChanges = hasChanges | layoutUpdated; - } - - if (lastVersion && lastVersion < v24) { - bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); - - if (disableHotkeysInFocus) { - config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); - } - - hasChanges = true; - } - - if (hasChanges) { - userConfig.SaveSafe("tmp"); - } -} - -static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; -static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; - -bool OBSApp::MigrateGlobalSettings() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), nullptr); - if (len <= 0) { - OBSErrorBox(nullptr, "Unable to get global configuration path."); - return false; - } - - std::string legacyConfigFileString; - legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); - legacyConfigFileString.append(path).append(OBSGlobalIniPath); - - const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); - - std::string configFileString; - configFileString.reserve(strlen(path) + OBSUserIniPath.size()); - configFileString.append(path).append(OBSUserIniPath); - - const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); - - if (std::filesystem::exists(userConfigFile)) { - OBSErrorBox(nullptr, - "Unable to migrate global configuration - user configuration file already exists."); - return false; - } - - try { - std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); - } catch (const std::filesystem::filesystem_error &) { - OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); - return false; - } - - return true; -} - -bool OBSApp::InitLocale() -{ - ProfileScope("OBSApp::InitLocale"); - - const char *lang = config_get_string(userConfig, "General", "Language"); - bool userLocale = config_has_user_value(userConfig, "General", "Language"); - if (!userLocale || !lang || lang[0] == '\0') - lang = DEFAULT_LANG; - - locale = lang; - - // set basic default application locale - if (!locale.empty()) - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - - string englishPath; - if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { - OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); - return false; - } - - textLookup = text_lookup_create(englishPath.c_str()); - if (!textLookup) { - OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); - return false; - } - - bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; - - if (userLocale && defaultLang) - return true; - - if (!userLocale && defaultLang) { - for (auto &locale_ : GetPreferredLocales()) { - if (locale_ == lang) - return true; - - stringstream file; - file << "locale/" << locale_ << ".ini"; - - string path; - if (!GetDataFilePath(file.str().c_str(), path)) - continue; - - if (!text_lookup_add(textLookup, path.c_str())) - continue; - - blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); - locale = locale_; - - // set application default locale to the new choosen one - if (!locale.empty()) - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - - return true; - } - - return true; - } - - stringstream file; - file << "locale/" << lang << ".ini"; - - string path; - if (GetDataFilePath(file.str().c_str(), path)) { - if (!text_lookup_add(textLookup, path.c_str())) - blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); - } else { - blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); - } - - return true; -} - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) -{ - JsonBranches branches; - - try { - nlohmann::json json = nlohmann::json::parse(jsonString); - branches = json.get(); - } catch (nlohmann::json::exception &e) { - error = e.what(); - return; - } - - for (const JsonBranch &json_branch : branches) { -#ifdef _WIN32 - if (!json_branch.windows) - continue; -#elif defined(__APPLE__) - if (!json_branch.macos) - continue; -#endif - - UpdateBranch branch = { - QString::fromStdString(json_branch.name), - QString::fromStdString(json_branch.display_name), - QString::fromStdString(json_branch.description), - json_branch.enabled, - json_branch.visible, - }; - out.push_back(branch); - } -} - -bool LoadBranchesFile(vector &out) -{ - string error; - string branchesText; - - BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); - - QFile branchesFile(branchesFilePath.Get()); - if (!branchesFile.open(QIODevice::ReadOnly)) { - error = "Opening file failed."; - goto fail; - } - - branchesText = branchesFile.readAll(); - if (branchesText.empty()) { - error = "File empty."; - goto fail; - } - - ParseBranchesJson(branchesText, out, error); - if (error.empty()) - return !out.empty(); - -fail: - blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); - return false; -} -#endif - -void OBSApp::SetBranchData(const string &data) -{ -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - string error; - vector result; - - ParseBranchesJson(data, result, error); - - if (!error.empty()) { - blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); - return; - } - - if (!result.empty()) - updateBranches = result; - - branches_loaded = true; -#else - UNUSED_PARAMETER(data); -#endif -} - -std::vector OBSApp::GetBranches() -{ - vector out; - /* Always ensure the default branch exists */ - out.push_back(UpdateBranch{"stable", "", "", true, true}); - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - if (!branches_loaded) { - vector result; - if (LoadBranchesFile(result)) - updateBranches = result; - - branches_loaded = true; - } -#endif - - /* Copy additional branches to result (if any) */ - if (!updateBranches.empty()) - out.insert(out.end(), updateBranches.begin(), updateBranches.end()); - - return out; -} - -OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) - : QApplication(argc, argv), - profilerNameStore(store) -{ - /* fix float handling */ -#if defined(Q_OS_UNIX) - if (!setlocale(LC_NUMERIC, "C")) - blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); -#endif - -#ifndef _WIN32 - /* Handle SIGINT properly */ - 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); -#endif - - sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); - -#ifndef __APPLE__ - setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); -#endif - - setDesktopFileName("com.obsproject.Studio"); -} - -OBSApp::~OBSApp() -{ -#ifdef _WIN32 - bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); - if (disableAudioDucking) - DisableAudioDucking(false); -#else - delete snInt; - close(sigintFd[0]); - close(sigintFd[1]); -#endif - -#ifdef __APPLE__ - bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); - bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); - if (vsyncDisabled && resetVSync) - EnableOSXVSync(true); -#endif - - os_inhibit_sleep_set_active(sleepInhibitor, false); - os_inhibit_sleep_destroy(sleepInhibitor); - - if (libobs_initialized) - obs_shutdown(); -} - -static void move_basic_to_profiles(void) -{ - char path[512]; - - if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { - return; - } - - const std::filesystem::path basicPath = std::filesystem::u8path(path); - - if (!std::filesystem::exists(basicPath)) { - return; - } - - const std::filesystem::path profilesPath = - App()->userProfilesLocation / std::filesystem::u8path("obs-studio/basic/profiles"); - - if (std::filesystem::exists(profilesPath)) { - return; - } - - try { - std::filesystem::create_directories(profilesPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create profiles directory for migration from basic profile\n%s", - error.what()); - return; - } - - const std::filesystem::path newProfilePath = profilesPath / std::filesystem::u8path(Str("Untitled")); - - for (auto &entry : std::filesystem::directory_iterator(basicPath)) { - if (entry.is_directory()) { - continue; - } - - if (entry.path().filename().u8string() == "scenes.json") { - continue; - } - - if (!std::filesystem::exists(newProfilePath)) { - try { - std::filesystem::create_directory(newProfilePath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create profile directory for 'Untitled'\n%s", error.what()); - return; - } - } - - const filesystem::path destinationFile = newProfilePath / entry.path().filename(); - - const auto copyOptions = std::filesystem::copy_options::overwrite_existing; - - try { - std::filesystem::copy(entry.path(), destinationFile, copyOptions); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s", - entry.path().filename().u8string().c_str(), error.what()); - - return; - } - } -} - -static void move_basic_to_scene_collections(void) -{ - char path[512]; - - if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { - return; - } - - const std::filesystem::path basicPath = std::filesystem::u8path(path); - - if (!std::filesystem::exists(basicPath)) { - return; - } - - const std::filesystem::path sceneCollectionPath = - App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); - - if (std::filesystem::exists(sceneCollectionPath)) { - return; - } - - try { - std::filesystem::create_directories(sceneCollectionPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, - "Failed to create scene collection directory for migration from basic scene collection\n%s", - error.what()); - return; - } - - const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); - const std::filesystem::path destinationFile = - (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); - - try { - std::filesystem::rename(sourceFile, destinationFile); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); - return; - } -} - -void OBSApp::AppInit() -{ - ProfileScope("OBSApp::AppInit"); - - if (!MakeUserDirs()) - throw "Failed to create required user directories"; - if (!InitGlobalConfig()) - throw "Failed to initialize global config"; - if (!InitLocale()) - throw "Failed to load locale"; - if (!InitTheme()) - throw "Failed to load theme"; - - config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); - config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); - - if (!config_has_user_value(userConfig, "Basic", "Profile")) { - config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); - config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); - } - - if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { - config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); - config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); - } - -#ifdef _WIN32 - bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); - if (disableAudioDucking) - DisableAudioDucking(true); -#endif - -#ifdef __APPLE__ - if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) - EnableOSXVSync(false); -#endif - - UpdateHotkeyFocusSetting(false); - - move_basic_to_profiles(); - move_basic_to_scene_collections(); - - if (!MakeUserProfileDirs()) - throw "Failed to create profile directories"; -} - -const char *OBSApp::GetRenderModule() const -{ - const char *renderer = config_get_string(appConfig, "Video", "Renderer"); - - return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; -} - -static bool StartupOBS(const char *locale, profiler_name_store_t *store) -{ - char path[512]; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) - return false; - - return obs_startup(locale, path, store); -} - -inline void OBSApp::ResetHotkeyState(bool inFocus) -{ - obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); -} - -void OBSApp::UpdateHotkeyFocusSetting(bool resetState) -{ - enableHotkeysInFocus = true; - enableHotkeysOutOfFocus = true; - - const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); - - if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { - enableHotkeysInFocus = false; - } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { - enableHotkeysOutOfFocus = false; - } - - if (resetState) - ResetHotkeyState(applicationState() == Qt::ApplicationActive); -} - -void OBSApp::DisableHotkeys() -{ - enableHotkeysInFocus = false; - enableHotkeysOutOfFocus = false; - ResetHotkeyState(applicationState() == Qt::ApplicationActive); -} - -Q_DECLARE_METATYPE(VoidFunc) - -void OBSApp::Exec(VoidFunc func) -{ - func(); -} - -static void ui_task_handler(obs_task_t task, void *param, bool wait) -{ - auto doTask = [=]() { - /* to get clang-format to behave */ - task(param); - }; - QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); -} - -bool OBSApp::OBSInit() -{ - ProfileScope("OBSApp::OBSInit"); - - qRegisterMetaType("VoidFunc"); - -#if !defined(_WIN32) && !defined(__APPLE__) - if (QApplication::platformName() == "xcb") { - obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); - blog(LOG_INFO, "Using EGL/X11"); - } - -#ifdef ENABLE_WAYLAND - if (QApplication::platformName().contains("wayland")) { - obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); - setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); - blog(LOG_INFO, "Platform: Wayland"); - } -#endif - - QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); - obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); -#endif - -#ifdef __APPLE__ - setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); -#endif - - if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) - return false; - - libobs_initialized = true; - - obs_set_ui_task_handler(ui_task_handler); - -#if defined(_WIN32) || defined(__APPLE__) - bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); - obs_apply_private_data(settings); - - blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); - - blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); -#endif -#ifdef _WIN32 - bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); - blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); -#endif - - blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); - blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); - - if (safe_mode) { - blog(LOG_WARNING, "Safe Mode enabled."); - } else if (disable_3p_plugins) { - blog(LOG_WARNING, "Third-party plugins disabled."); - } - - setQuitOnLastWindowClosed(false); - - mainWindow = new OBSBasic(); - - mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); - connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); - - mainWindow->OBSInit(); - - connect(this, &QGuiApplication::applicationStateChanged, - [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); - ResetHotkeyState(applicationState() == Qt::ApplicationActive); - return true; -} - -string OBSApp::GetVersionString(bool platform) const -{ - stringstream ver; - -#ifdef HAVE_OBSCONFIG_H - ver << obs_get_version_string(); -#else - ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; - -#endif - - if (platform) { - ver << " ("; -#ifdef _WIN32 - if (sizeof(void *) == 8) - ver << "64-bit, "; - else - ver << "32-bit, "; - - ver << "windows)"; -#elif __APPLE__ - ver << "mac)"; -#elif __OpenBSD__ - ver << "openbsd)"; -#elif __FreeBSD__ - ver << "freebsd)"; -#else /* assume linux for the time being */ - ver << "linux)"; -#endif - } - - return ver.str(); -} - -bool OBSApp::IsPortableMode() -{ - return portable_mode; -} - -bool OBSApp::IsUpdaterDisabled() -{ - return opt_disable_updater; -} - -bool OBSApp::IsMissingFilesCheckDisabled() -{ - return opt_disable_missing_files_check; -} - -#ifdef __APPLE__ -#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" -#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" -#elif _WIN32 -#define INPUT_AUDIO_SOURCE "wasapi_input_capture" -#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" -#else -#define INPUT_AUDIO_SOURCE "pulse_input_capture" -#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" -#endif - -const char *OBSApp::InputAudioSource() const -{ - return INPUT_AUDIO_SOURCE; -} - -const char *OBSApp::OutputAudioSource() const -{ - return OUTPUT_AUDIO_SOURCE; -} - -const char *OBSApp::GetLastLog() const -{ - return lastLogFile.c_str(); -} - -const char *OBSApp::GetCurrentLog() const -{ - return currentLogFile.c_str(); -} - -const char *OBSApp::GetLastCrashLog() const -{ - return lastCrashLogFile.c_str(); -} - -bool OBSApp::TranslateString(const char *lookupVal, const char **out) const -{ - for (obs_frontend_translate_ui_cb cb : translatorHooks) { - if (cb(lookupVal, out)) - return true; - } - - return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); -} - -// Global handler to receive all QEvent::Show events so we can apply -// display affinity on any newly created windows and dialogs without -// caring where they are coming from (e.g. plugins). -bool OBSApp::notify(QObject *receiver, QEvent *e) -{ - QWidget *w; - QWindow *window; - int windowType; - - if (!receiver->isWidgetType()) - goto skip; - - if (e->type() != QEvent::Show) - goto skip; - - w = qobject_cast(receiver); - - if (!w->isWindow()) - goto skip; - - window = w->windowHandle(); - if (!window) - goto skip; - - windowType = window->flags() & Qt::WindowType::WindowType_Mask; - - if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || - windowType == Qt::WindowType::Tool) { - OBSBasic *main = reinterpret_cast(GetMainWindow()); - if (main) - main->SetDisplayAffinity(window); - } - -skip: - return QApplication::notify(receiver, e); -} - -QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const -{ - const char *out = nullptr; - QString str(sourceText); - str.replace(" ", ""); - if (!App()->TranslateString(QT_TO_UTF8(str), &out)) - return QString(sourceText); - - return QT_UTF8(out); -} - static bool get_token(lexer *lex, string &str, base_token_type type) { base_token token; @@ -1603,189 +400,6 @@ static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string } } -string GenerateTimeDateFilename(const char *extension, bool noSpace) -{ - time_t now = time(0); - char file[256] = {}; - struct tm *cur_time; - - cur_time = localtime(&now); - snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, - cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, - extension); - - return string(file); -} - -string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) -{ - BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); - return string(filename); -} - -static void FindBestFilename(string &strPath, bool noSpace) -{ - int num = 2; - - if (!os_file_exists(strPath.c_str())) - return; - - const char *ext = strrchr(strPath.c_str(), '.'); - if (!ext) - return; - - int extStart = int(ext - strPath.c_str()); - for (;;) { - string testPath = strPath; - string numStr; - - numStr = noSpace ? "_" : " ("; - numStr += to_string(num++); - if (!noSpace) - numStr += ")"; - - testPath.insert(extStart, numStr); - - if (!os_file_exists(testPath.c_str())) { - strPath = testPath; - break; - } - } -} - -static void ensure_directory_exists(string &path) -{ - replace(path.begin(), path.end(), '\\', '/'); - - size_t last = path.rfind('/'); - if (last == string::npos) - return; - - string directory = path.substr(0, last); - os_mkdirs(directory.c_str()); -} - -static void remove_reserved_file_characters(string &s) -{ - replace(s.begin(), s.end(), '\\', '/'); - replace(s.begin(), s.end(), '*', '_'); - replace(s.begin(), s.end(), '?', '_'); - replace(s.begin(), s.end(), '"', '_'); - replace(s.begin(), s.end(), '|', '_'); - replace(s.begin(), s.end(), ':', '_'); - replace(s.begin(), s.end(), '>', '_'); - replace(s.begin(), s.end(), '<', '_'); -} - -string GetFormatString(const char *format, const char *prefix, const char *suffix) -{ - string f; - - f = format; - - if (prefix && *prefix) { - string str_prefix = prefix; - - if (str_prefix.back() != ' ') - str_prefix += " "; - - size_t insert_pos = 0; - size_t tmp; - - tmp = f.find_last_of('/'); - if (tmp != string::npos && tmp > insert_pos) - insert_pos = tmp + 1; - - tmp = f.find_last_of('\\'); - if (tmp != string::npos && tmp > insert_pos) - insert_pos = tmp + 1; - - f.insert(insert_pos, str_prefix); - } - - if (suffix && *suffix) { - if (*suffix != ' ') - f += " "; - f += suffix; - } - - remove_reserved_file_characters(f); - - return f; -} - -string GetFormatExt(const char *container) -{ - string ext = container; - if (ext == "fragmented_mp4") - ext = "mp4"; - if (ext == "hybrid_mp4") - ext = "mp4"; - else if (ext == "fragmented_mov") - ext = "mov"; - else if (ext == "hls") - ext = "m3u8"; - else if (ext == "mpegts") - ext = "ts"; - - return ext; -} - -string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; - - if (!dir) { - if (main->isVisible()) - OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); - else - main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); - return ""; - } - - os_closedir(dir); - - string strPath; - strPath += path; - - char lastChar = strPath.back(); - if (lastChar != '/' && lastChar != '\\') - strPath += "/"; - - string ext = GetFormatExt(container); - strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); - ensure_directory_exists(strPath); - if (!overwrite) - FindBestFilename(strPath, noSpace); - - return strPath; -} - -vector> GetLocaleNames() -{ - string path; - if (!GetDataFilePath("locale.ini", path)) - throw "Could not find locale.ini path"; - - ConfigFile ini; - if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) - throw "Could not open locale.ini"; - - size_t sections = config_num_sections(ini); - - vector> names; - names.reserve(sections); - for (size_t i = 0; i < sections; i++) { - const char *tag = config_get_section(ini, i); - const char *name = config_get_string(ini, tag, "Name"); - names.emplace_back(tag, name); - } - - return names; -} - static void create_log_file(fstream &logFile) { stringstream dst; @@ -2223,134 +837,6 @@ static void load_debug_privilege(void) } #endif -#ifdef __APPLE__ -#define BASE_PATH ".." -#else -#define BASE_PATH "../.." -#endif - -#define CONFIG_PATH BASE_PATH "/config" - -#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) -#define ALLOW_PORTABLE_MODE 1 -#else -#define ALLOW_PORTABLE_MODE 0 -#endif - -int GetAppConfigPath(char *path, size_t size, const char *name) -{ -#if ALLOW_PORTABLE_MODE - if (portable_mode) { - if (name && *name) { - return snprintf(path, size, CONFIG_PATH "/%s", name); - } else { - return snprintf(path, size, CONFIG_PATH); - } - } else { - return os_get_config_path(path, size, name); - } -#else - return os_get_config_path(path, size, name); -#endif -} - -char *GetAppConfigPathPtr(const char *name) -{ -#if ALLOW_PORTABLE_MODE - if (portable_mode) { - char path[512]; - - if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { - return bstrdup(path); - } else { - return NULL; - } - } else { - return os_get_config_path_ptr(name); - } -#else - return os_get_config_path_ptr(name); -#endif -} - -int GetProgramDataPath(char *path, size_t size, const char *name) -{ - return os_get_program_data_path(path, size, name); -} - -char *GetProgramDataPathPtr(const char *name) -{ - return os_get_program_data_path_ptr(name); -} - -bool GetFileSafeName(const char *name, std::string &file) -{ - size_t base_len = strlen(name); - size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); - std::wstring wfile; - - if (!len) - return false; - - wfile.resize(len); - os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); - - for (size_t i = wfile.size(); i > 0; i--) { - size_t im1 = i - 1; - - if (iswspace(wfile[im1])) { - wfile[im1] = '_'; - } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { - wfile.erase(im1, 1); - } - } - - if (wfile.size() == 0) - wfile = L"characters_only"; - - len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); - if (!len) - return false; - - file.resize(len); - os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); - return true; -} - -bool GetClosestUnusedFileName(std::string &path, const char *extension) -{ - size_t len = path.size(); - if (extension) { - path += "."; - path += extension; - } - - if (!os_file_exists(path.c_str())) - return true; - - int index = 1; - - do { - path.resize(len); - path += std::to_string(++index); - if (extension) { - path += "."; - path += extension; - } - } while (os_file_exists(path.c_str())); - - return true; -} - -bool WindowPositionValid(QRect rect) -{ - for (QScreen *screen : QGuiApplication::screens()) { - if (screen->availableGeometry().intersects(rect)) - return true; - } - return false; -} - static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) { return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); @@ -2386,44 +872,6 @@ static void delete_safe_mode_sentinel(void) #endif } -#ifndef _WIN32 -void OBSApp::SigIntSignalHandler(int s) -{ - /* Handles SIGINT and writes to a socket. Qt will read - * from the socket in the main thread event loop and trigger - * a call to the ProcessSigInt slot, where we can safely run - * shutdown code without signal safety issues. */ - UNUSED_PARAMETER(s); - - char a = 1; - send(sigintFd[0], &a, sizeof(a), 0); -} -#endif - -void OBSApp::ProcessSigInt(void) -{ - /* This looks weird, but we can't ifdef a Qt slot function so - * the SIGINT handler simply does nothing on Windows. */ -#ifndef _WIN32 - char tmp; - recv(sigintFd[1], &tmp, sizeof(tmp), 0); - - OBSBasic *main = reinterpret_cast(GetMainWindow()); - if (main) - main->close(); -#endif -} - -#ifdef _WIN32 -void OBSApp::commitData(QSessionManager &manager) -{ - if (auto main = App()->GetMainWindow()) { - QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); - manager.cancel(); - } -} -#endif - #ifdef _WIN32 static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " @@ -2449,6 +897,18 @@ static bool vc_runtime_outdated() } #endif +#if defined(__APPLE__) || defined(__linux__) +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + int main(int argc, char *argv[]) { #ifndef _WIN32 diff --git a/frontend/utility/BaseLexer.hpp b/frontend/utility/BaseLexer.hpp index dea5e530b..c32156b2a 100644 --- a/frontend/utility/BaseLexer.hpp +++ b/frontend/utility/BaseLexer.hpp @@ -17,41 +17,7 @@ #pragma once -#include -#include -#include -#include - -#ifndef _WIN32 -#include -#else -#include -#endif -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "window-main.hpp" -#include "obs-app-theming.hpp" - -std::string CurrentTimeString(); -std::string CurrentDateTimeString(); -std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); -std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); -std::string GetFormatString(const char *format, const char *prefix, const char *suffix); -std::string GetFormatExt(const char *container); -std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, - const char *format); -QObject *CreateShortcutFilter(); struct BaseLexer { lexer lex; @@ -61,220 +27,3 @@ public: inline ~BaseLexer() { lexer_free(&lex); } operator lexer *() { return &lex; } }; - -class OBSTranslator : public QTranslator { - Q_OBJECT - -public: - virtual bool isEmpty() const override { return false; } - - virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, - int n) const override; -}; - -typedef std::function VoidFunc; - -struct UpdateBranch { - QString name; - QString display_name; - QString description; - bool is_enabled; - bool is_visible; -}; - -class OBSApp : public QApplication { - Q_OBJECT - -private: - std::string locale; - - ConfigFile appConfig; - ConfigFile userConfig; - TextLookup textLookup; - QPointer mainWindow; - profiler_name_store_t *profilerNameStore = nullptr; - std::vector updateBranches; - bool branches_loaded = false; - - bool libobs_initialized = false; - - os_inhibit_t *sleepInhibitor = nullptr; - int sleepInhibitRefs = 0; - - bool enableHotkeysInFocus = true; - bool enableHotkeysOutOfFocus = true; - - std::deque translatorHooks; - - bool UpdatePre22MultiviewLayout(const char *layout); - - bool InitGlobalConfig(); - bool InitGlobalConfigDefaults(); - bool InitGlobalLocationDefaults(); - - bool MigrateGlobalSettings(); - void MigrateLegacySettings(uint32_t lastVersion); - - bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); - void InitUserConfigDefaults(); - - bool InitLocale(); - bool InitTheme(); - - inline void ResetHotkeyState(bool inFocus); - - QPalette defaultPalette; - OBSTheme *currentTheme = nullptr; - QHash themes; - QPointer themeWatcher; - - void FindThemes(); - - bool notify(QObject *receiver, QEvent *e) override; - -#ifndef _WIN32 - static int sigintFd[2]; - QSocketNotifier *snInt = nullptr; -#else -private slots: - void commitData(QSessionManager &manager); -#endif - -private slots: - void themeFileChanged(const QString &); - -public: - OBSApp(int &argc, char **argv, profiler_name_store_t *store); - ~OBSApp(); - - void AppInit(); - bool OBSInit(); - - void UpdateHotkeyFocusSetting(bool reset = true); - void DisableHotkeys(); - - inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } - - inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } - - inline config_t *GetAppConfig() const { return appConfig; } - inline config_t *GetUserConfig() const { return userConfig; } - std::filesystem::path userConfigLocation; - std::filesystem::path userScenesLocation; - std::filesystem::path userProfilesLocation; - - inline const char *GetLocale() const { return locale.c_str(); } - - OBSTheme *GetTheme() const { return currentTheme; } - QList GetThemes() const { return themes.values(); } - OBSTheme *GetTheme(const QString &name); - bool SetTheme(const QString &name); - bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } - - void SetBranchData(const std::string &data); - std::vector GetBranches(); - - inline lookup_t *GetTextLookup() const { return textLookup; } - - inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } - - bool TranslateString(const char *lookupVal, const char **out) const; - - profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } - - const char *GetLastLog() const; - const char *GetCurrentLog() const; - - const char *GetLastCrashLog() const; - - std::string GetVersionString(bool platform = true) const; - bool IsPortableMode(); - bool IsUpdaterDisabled(); - bool IsMissingFilesCheckDisabled(); - - const char *InputAudioSource() const; - const char *OutputAudioSource() const; - - const char *GetRenderModule() const; - - inline void IncrementSleepInhibition() - { - if (!sleepInhibitor) - return; - if (sleepInhibitRefs++ == 0) - os_inhibit_sleep_set_active(sleepInhibitor, true); - } - - inline void DecrementSleepInhibition() - { - if (!sleepInhibitor) - return; - if (sleepInhibitRefs == 0) - return; - if (--sleepInhibitRefs == 0) - os_inhibit_sleep_set_active(sleepInhibitor, false); - } - - inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } - - inline void PopUITranslation() { translatorHooks.pop_front(); } -#ifndef _WIN32 - static void SigIntSignalHandler(int); -#endif - -public slots: - void Exec(VoidFunc func); - void ProcessSigInt(); - -signals: - void StyleChanged(); -}; - -int GetAppConfigPath(char *path, size_t size, const char *name); -char *GetAppConfigPathPtr(const char *name); - -int GetProgramDataPath(char *path, size_t size, const char *name); -char *GetProgramDataPathPtr(const char *name); - -inline OBSApp *App() -{ - return static_cast(qApp); -} - -std::vector> GetLocaleNames(); -inline const char *Str(const char *lookup) -{ - return App()->GetString(lookup); -} -inline QString QTStr(const char *lookupVal) -{ - return QString::fromUtf8(Str(lookupVal)); -} - -bool GetFileSafeName(const char *name, std::string &file); -bool GetClosestUnusedFileName(std::string &path, const char *extension); -bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); - -bool WindowPositionValid(QRect rect); - -extern bool portable_mode; -extern bool steam; -extern bool safe_mode; -extern bool disable_3p_plugins; - -extern bool opt_start_streaming; -extern bool opt_start_recording; -extern bool opt_start_replaybuffer; -extern bool opt_start_virtualcam; -extern bool opt_minimize_tray; -extern bool opt_studio_mode; -extern bool opt_allow_opengl; -extern bool opt_always_on_top; -extern std::string opt_starting_scene; -extern bool restart; -extern bool restart_safe; - -#ifdef _WIN32 -extern "C" void install_dll_blocklist_hook(void); -extern "C" void log_blocked_dlls(void); -#endif diff --git a/frontend/utility/OBSTheme.hpp b/frontend/utility/OBSTheme.hpp index cbaed7f24..dbb3b0a6c 100644 --- a/frontend/utility/OBSTheme.hpp +++ b/frontend/utility/OBSTheme.hpp @@ -17,12 +17,11 @@ #pragma once -#include +#include +#include #include -struct OBSThemeVariable; - struct OBSTheme { /* internal name, must be unique */ QString id; @@ -43,24 +42,3 @@ struct OBSTheme { bool isBaseTheme; /* Whether it is a "style" or variant */ bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ }; - -struct OBSThemeVariable { - enum VariableType { - Color, /* RGB color value*/ - Size, /* Number with suffix denoting size (e.g. px, pt, em) */ - Number, /* Number without suffix */ - String, /* Raw string (e.g. color name, border style, etc.) */ - Alias, /* Points at another variable, value will be the key */ - Calc, /* Simple calculation with two operands */ - }; - - /* Whether the variable should be editable in the UI */ - bool editable = false; - /* Used for VariableType::Size only */ - QString suffix; - - VariableType type; - QString name; - QVariant value; - QVariant userValue; /* If overwritten by user, use this value instead */ -}; diff --git a/frontend/utility/OBSThemeVariable.hpp b/frontend/utility/OBSThemeVariable.hpp index cbaed7f24..f04f90cc5 100644 --- a/frontend/utility/OBSThemeVariable.hpp +++ b/frontend/utility/OBSThemeVariable.hpp @@ -17,33 +17,9 @@ #pragma once +#include #include -#include - -struct OBSThemeVariable; - -struct OBSTheme { - /* internal name, must be unique */ - QString id; - QString name; - QString author; - QString extends; - - /* First ancestor base theme */ - QString parent; - /* Dependencies from root to direct ancestor */ - QStringList dependencies; - /* File path */ - std::filesystem::path location; - std::filesystem::path filename; /* Filename without extension */ - - bool isDark; - bool isVisible; /* Whether it should be shown to the user */ - bool isBaseTheme; /* Whether it is a "style" or variant */ - bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ -}; - struct OBSThemeVariable { enum VariableType { Color, /* RGB color value*/ diff --git a/frontend/utility/OBSTranslator.cpp b/frontend/utility/OBSTranslator.cpp index 883bee21a..f61354cce 100644 --- a/frontend/utility/OBSTranslator.cpp +++ b/frontend/utility/OBSTranslator.cpp @@ -15,1427 +15,13 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSTranslator.hpp" + +#include + #include -#include -#include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "obs-proxy-style.hpp" -#include "log-viewer.hpp" -#include "volume-control.hpp" -#include "window-basic-main.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-basic-settings.hpp" -#include "platform.hpp" - -#include - -#include - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#include -#include -#include -#endif - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -#include "update/models/branches.hpp" -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) -#include -#include -#endif - -#include - -#include "ui-config.h" - -using namespace std; - -static log_handler_t def_log_handler; - -static string currentLogFile; -static string lastLogFile; -static string lastCrashLogFile; - -bool portable_mode = false; -bool steam = false; -bool safe_mode = false; -bool disable_3p_plugins = false; -bool unclean_shutdown = false; -bool disable_shutdown_check = false; -static bool multi = false; -static bool log_verbose = false; -static bool unfiltered_log = false; -bool opt_start_streaming = false; -bool opt_start_recording = false; -bool opt_studio_mode = false; -bool opt_start_replaybuffer = false; -bool opt_start_virtualcam = false; -bool opt_minimize_tray = false; -bool opt_allow_opengl = false; -bool opt_always_on_top = false; -bool opt_disable_updater = false; -bool opt_disable_missing_files_check = false; -string opt_starting_collection; -string opt_starting_profile; -string opt_starting_scene; - -bool restart = false; -bool restart_safe = false; -QStringList arguments; - -QPointer obsLogViewer; - -#ifndef _WIN32 -int OBSApp::sigintFd[2]; -#endif - -// GPU hint exports for AMD/NVIDIA laptops -#ifdef _MSC_VER -extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; -extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; -#endif - -QObject *CreateShortcutFilter() -{ - return new OBSEventFilter([](QObject *obj, QEvent *event) { - auto mouse_event = [](QMouseEvent &event) { - if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) - return true; - - obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; - bool pressed = event.type() == QEvent::MouseButtonPress; - - switch (event.button()) { - case Qt::NoButton: - case Qt::LeftButton: - case Qt::RightButton: - case Qt::AllButtons: - case Qt::MouseButtonMask: - return false; - - case Qt::MiddleButton: - hotkey.key = OBS_KEY_MOUSE3; - break; - -#define MAP_BUTTON(i, j) \ - case Qt::ExtraButton##i: \ - hotkey.key = OBS_KEY_MOUSE##j; \ - break; - MAP_BUTTON(1, 4); - MAP_BUTTON(2, 5); - MAP_BUTTON(3, 6); - MAP_BUTTON(4, 7); - MAP_BUTTON(5, 8); - MAP_BUTTON(6, 9); - MAP_BUTTON(7, 10); - MAP_BUTTON(8, 11); - MAP_BUTTON(9, 12); - MAP_BUTTON(10, 13); - MAP_BUTTON(11, 14); - MAP_BUTTON(12, 15); - MAP_BUTTON(13, 16); - MAP_BUTTON(14, 17); - MAP_BUTTON(15, 18); - MAP_BUTTON(16, 19); - MAP_BUTTON(17, 20); - MAP_BUTTON(18, 21); - MAP_BUTTON(19, 22); - MAP_BUTTON(20, 23); - MAP_BUTTON(21, 24); - MAP_BUTTON(22, 25); - MAP_BUTTON(23, 26); - MAP_BUTTON(24, 27); -#undef MAP_BUTTON - } - - hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); - - obs_hotkey_inject_event(hotkey, pressed); - return true; - }; - - auto key_event = [&](QKeyEvent *event) { - int key = event->key(); - bool enabledInFocus = App()->HotkeysEnabledInFocus(); - - if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) - return true; - - QDialog *dialog = qobject_cast(obj); - - obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; - bool pressed = event->type() == QEvent::KeyPress; - - switch (key) { - case Qt::Key_Shift: - case Qt::Key_Control: - case Qt::Key_Alt: - case Qt::Key_Meta: - break; - -#ifdef __APPLE__ - case Qt::Key_CapsLock: - // kVK_CapsLock == 57 - hotkey.key = obs_key_from_virtual_key(57); - pressed = true; - break; -#endif - - case Qt::Key_Enter: - case Qt::Key_Escape: - case Qt::Key_Return: - if (dialog && pressed) - return false; - if (!enabledInFocus) - return true; - /* Falls through. */ - default: - hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); - } - - if (event->isAutoRepeat()) - return true; - - hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - obs_hotkey_inject_event(hotkey, pressed); - return true; - }; - - switch (event->type()) { - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - return mouse_event(*static_cast(event)); - - /*case QEvent::MouseButtonDblClick: - case QEvent::Wheel:*/ - case QEvent::KeyPress: - case QEvent::KeyRelease: - return key_event(static_cast(event)); - - default: - return false; - } - }); -} - -string CurrentTimeString() -{ - using namespace std::chrono; - - struct tm tstruct; - char buf[80]; - - auto tp = system_clock::now(); - auto now = system_clock::to_time_t(tp); - tstruct = *localtime(&now); - - size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); - if (ratio_less::value && written && (sizeof(buf) - written) > 5) { - auto tp_secs = time_point_cast(tp); - auto millis = duration_cast(tp - tp_secs).count(); - - snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); - } - - return buf; -} - -string CurrentDateTimeString() -{ - time_t now = time(0); - struct tm tstruct; - char buf[80]; - tstruct = *localtime(&now); - strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); - return buf; -} - -static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) -{ - static mutex logfile_mutex; - string msg; - msg += timeString; - msg += str; - - logfile_mutex.lock(); - logFile << msg << endl; - logfile_mutex.unlock(); - - if (!!obsLogViewer) - QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), - Q_ARG(QString, QString(msg.c_str()))); -} - -static inline void LogStringChunk(fstream &logFile, char *str, int log_level) -{ - char *nextLine = str; - string timeString = CurrentTimeString(); - timeString += ": "; - - while (*nextLine) { - char *nextLine = strchr(str, '\n'); - if (!nextLine) - break; - - if (nextLine != str && nextLine[-1] == '\r') { - nextLine[-1] = 0; - } else { - nextLine[0] = 0; - } - - LogString(logFile, timeString.c_str(), str, log_level); - nextLine++; - str = nextLine; - } - - LogString(logFile, timeString.c_str(), str, log_level); -} - -#define MAX_REPEATED_LINES 30 -#define MAX_CHAR_VARIATION (255 * 3) - -static inline int sum_chars(const char *str) -{ - int val = 0; - for (; *str != 0; str++) - val += *str; - - return val; -} - -static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) -{ - static mutex log_mutex; - static const char *last_msg_ptr = nullptr; - static int last_char_sum = 0; - static int rep_count = 0; - - int new_sum = sum_chars(output_str); - - lock_guard guard(log_mutex); - - if (unfiltered_log) { - return false; - } - - if (last_msg_ptr == msg) { - int diff = std::abs(new_sum - last_char_sum); - if (diff < MAX_CHAR_VARIATION) { - return (rep_count++ >= MAX_REPEATED_LINES); - } - } - - if (rep_count > MAX_REPEATED_LINES) { - logFile << CurrentTimeString() << ": Last log entry repeated for " - << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; - } - - last_msg_ptr = msg; - last_char_sum = new_sum; - rep_count = 0; - - return false; -} - -static void do_log(int log_level, const char *msg, va_list args, void *param) -{ - fstream &logFile = *static_cast(param); - char str[8192]; - -#ifndef _WIN32 - va_list args2; - va_copy(args2, args); -#endif - - vsnprintf(str, sizeof(str), msg, args); - -#ifdef _WIN32 - if (IsDebuggerPresent()) { - int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); - if (wNum > 1) { - static wstring wide_buf; - static mutex wide_mutex; - - lock_guard lock(wide_mutex); - wide_buf.reserve(wNum + 1); - wide_buf.resize(wNum - 1); - MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); - wide_buf.push_back('\n'); - - OutputDebugStringW(wide_buf.c_str()); - } - } -#endif - -#if !defined(_WIN32) && defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - - if (log_level <= LOG_INFO || log_verbose) { -#if !defined(_WIN32) && !defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - if (!too_many_repeated_entries(logFile, msg, str)) - LogStringChunk(logFile, str, log_level); - } - -#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) - if (log_level <= LOG_ERROR && IsDebuggerPresent()) - __debugbreak(); -#endif - -#ifndef _WIN32 - va_end(args2); -#endif -} - -#define DEFAULT_LANG "en-US" - -bool OBSApp::InitGlobalConfigDefaults() -{ - config_set_default_uint(appConfig, "General", "MaxLogs", 10); - config_set_default_int(appConfig, "General", "InfoIncrement", -1); - config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); - config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); - -#if _WIN32 - config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); -#else - config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); -#endif - -#ifdef _WIN32 - config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); - config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); -#endif - -#ifdef __APPLE__ - config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); - config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); - config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); -#endif - - return true; -} - -bool OBSApp::InitGlobalLocationDefaults() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), nullptr); - if (len <= 0) { - OBSErrorBox(NULL, "Unable to get global configuration path."); - return false; - } - - config_set_default_string(appConfig, "Locations", "Configuration", path); - config_set_default_string(appConfig, "Locations", "SceneCollections", path); - config_set_default_string(appConfig, "Locations", "Profiles", path); - - return true; -} - -void OBSApp::InitUserConfigDefaults() -{ - config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); - - config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); - - config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); - config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); - config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); - config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); - config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); - config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); - config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); - config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); - config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); - config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); - config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); - config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); - config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); - config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); - config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); - - config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); - - config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); - - config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); -} - -static bool do_mkdir(const char *path) -{ - if (os_mkdirs(path) == MKDIR_ERROR) { - OBSErrorBox(NULL, "Failed to create directory %s", path); - return false; - } - - return true; -} - -static bool MakeUserDirs() -{ - char path[512]; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) - return false; - if (!do_mkdir(path)) - return false; - -#ifdef _WIN32 - if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) - return false; - if (!do_mkdir(path)) - return false; -#endif - -#ifdef WHATSNEW_ENABLED - if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) - return false; - if (!do_mkdir(path)) - return false; -#endif - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) - return false; - if (!do_mkdir(path)) - return false; - - return true; -} - -constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; -constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; - -static bool MakeUserProfileDirs() -{ - const std::filesystem::path userProfilePath = - App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); - const std::filesystem::path userScenesPath = - App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); - - if (!std::filesystem::exists(userProfilePath)) { - try { - std::filesystem::create_directories(userProfilePath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create user profile directory '%s'\n%s", - userProfilePath.u8string().c_str(), error.what()); - return false; - } - } - - if (!std::filesystem::exists(userScenesPath)) { - try { - std::filesystem::create_directories(userScenesPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create user scene collection directory '%s'\n%s", - userScenesPath.u8string().c_str(), error.what()); - return false; - } - } - - return true; -} - -bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) -{ - if (!layout) - return false; - - if (astrcmpi(layout, "horizontaltop") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "horizontalbottom") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "verticalleft") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); - return true; - } - - if (astrcmpi(layout, "verticalright") == 0) { - config_set_int(userConfig, "BasicWindow", "MultiviewLayout", - static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); - return true; - } - - return false; -} - -bool OBSApp::InitGlobalConfig() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); - if (len <= 0) { - return false; - } - - int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); - if (errorcode != CONFIG_SUCCESS) { - OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); - return false; - } - - uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); - - if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { - bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); - - if (!migratedUserSettings) { - bool migrated = MigrateGlobalSettings(); - - config_set_bool(appConfig, "General", "Pre31Migrated", migrated); - config_save_safe(appConfig, "tmp", nullptr); - } - } - - InitGlobalConfigDefaults(); - InitGlobalLocationDefaults(); - - if (IsPortableMode()) { - userConfigLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); - userScenesLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); - userProfilesLocation = - std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); - } else { - userConfigLocation = - std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); - userScenesLocation = - std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); - userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); - } - - bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); - - return userConfigResult; -} - -bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) -{ - const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; - - int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); - - if (errorCode != CONFIG_SUCCESS) { - OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); - return false; - } - - MigrateLegacySettings(lastVersion); - InitUserConfigDefaults(); - - return true; -} - -void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) -{ - bool hasChanges = false; - - const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); - const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); - const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); - const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); - const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); - - const map defaultsMap{ - {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; - - for (auto &[version, configKey] : defaultsMap) { - if (!config_has_user_value(userConfig, "General", configKey.c_str())) { - bool useOldDefaults = lastVersion && lastVersion < version; - config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); - - hasChanges = true; - } - } - - if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { - const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); - - bool layoutUpdated = UpdatePre22MultiviewLayout(layout); - - hasChanges = hasChanges | layoutUpdated; - } - - if (lastVersion && lastVersion < v24) { - bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); - - if (disableHotkeysInFocus) { - config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); - } - - hasChanges = true; - } - - if (hasChanges) { - userConfig.SaveSafe("tmp"); - } -} - -static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; -static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; - -bool OBSApp::MigrateGlobalSettings() -{ - char path[512]; - - int len = GetAppConfigPath(path, sizeof(path), nullptr); - if (len <= 0) { - OBSErrorBox(nullptr, "Unable to get global configuration path."); - return false; - } - - std::string legacyConfigFileString; - legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); - legacyConfigFileString.append(path).append(OBSGlobalIniPath); - - const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); - - std::string configFileString; - configFileString.reserve(strlen(path) + OBSUserIniPath.size()); - configFileString.append(path).append(OBSUserIniPath); - - const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); - - if (std::filesystem::exists(userConfigFile)) { - OBSErrorBox(nullptr, - "Unable to migrate global configuration - user configuration file already exists."); - return false; - } - - try { - std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); - } catch (const std::filesystem::filesystem_error &) { - OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); - return false; - } - - return true; -} - -bool OBSApp::InitLocale() -{ - ProfileScope("OBSApp::InitLocale"); - - const char *lang = config_get_string(userConfig, "General", "Language"); - bool userLocale = config_has_user_value(userConfig, "General", "Language"); - if (!userLocale || !lang || lang[0] == '\0') - lang = DEFAULT_LANG; - - locale = lang; - - // set basic default application locale - if (!locale.empty()) - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - - string englishPath; - if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { - OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); - return false; - } - - textLookup = text_lookup_create(englishPath.c_str()); - if (!textLookup) { - OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); - return false; - } - - bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; - - if (userLocale && defaultLang) - return true; - - if (!userLocale && defaultLang) { - for (auto &locale_ : GetPreferredLocales()) { - if (locale_ == lang) - return true; - - stringstream file; - file << "locale/" << locale_ << ".ini"; - - string path; - if (!GetDataFilePath(file.str().c_str(), path)) - continue; - - if (!text_lookup_add(textLookup, path.c_str())) - continue; - - blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); - locale = locale_; - - // set application default locale to the new choosen one - if (!locale.empty()) - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - - return true; - } - - return true; - } - - stringstream file; - file << "locale/" << lang << ".ini"; - - string path; - if (GetDataFilePath(file.str().c_str(), path)) { - if (!text_lookup_add(textLookup, path.c_str())) - blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); - } else { - blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); - } - - return true; -} - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) -{ - JsonBranches branches; - - try { - nlohmann::json json = nlohmann::json::parse(jsonString); - branches = json.get(); - } catch (nlohmann::json::exception &e) { - error = e.what(); - return; - } - - for (const JsonBranch &json_branch : branches) { -#ifdef _WIN32 - if (!json_branch.windows) - continue; -#elif defined(__APPLE__) - if (!json_branch.macos) - continue; -#endif - - UpdateBranch branch = { - QString::fromStdString(json_branch.name), - QString::fromStdString(json_branch.display_name), - QString::fromStdString(json_branch.description), - json_branch.enabled, - json_branch.visible, - }; - out.push_back(branch); - } -} - -bool LoadBranchesFile(vector &out) -{ - string error; - string branchesText; - - BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); - - QFile branchesFile(branchesFilePath.Get()); - if (!branchesFile.open(QIODevice::ReadOnly)) { - error = "Opening file failed."; - goto fail; - } - - branchesText = branchesFile.readAll(); - if (branchesText.empty()) { - error = "File empty."; - goto fail; - } - - ParseBranchesJson(branchesText, out, error); - if (error.empty()) - return !out.empty(); - -fail: - blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); - return false; -} -#endif - -void OBSApp::SetBranchData(const string &data) -{ -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - string error; - vector result; - - ParseBranchesJson(data, result, error); - - if (!error.empty()) { - blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); - return; - } - - if (!result.empty()) - updateBranches = result; - - branches_loaded = true; -#else - UNUSED_PARAMETER(data); -#endif -} - -std::vector OBSApp::GetBranches() -{ - vector out; - /* Always ensure the default branch exists */ - out.push_back(UpdateBranch{"stable", "", "", true, true}); - -#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) - if (!branches_loaded) { - vector result; - if (LoadBranchesFile(result)) - updateBranches = result; - - branches_loaded = true; - } -#endif - - /* Copy additional branches to result (if any) */ - if (!updateBranches.empty()) - out.insert(out.end(), updateBranches.begin(), updateBranches.end()); - - return out; -} - -OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) - : QApplication(argc, argv), - profilerNameStore(store) -{ - /* fix float handling */ -#if defined(Q_OS_UNIX) - if (!setlocale(LC_NUMERIC, "C")) - blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); -#endif - -#ifndef _WIN32 - /* Handle SIGINT properly */ - 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); -#endif - - sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); - -#ifndef __APPLE__ - setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); -#endif - - setDesktopFileName("com.obsproject.Studio"); -} - -OBSApp::~OBSApp() -{ -#ifdef _WIN32 - bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); - if (disableAudioDucking) - DisableAudioDucking(false); -#else - delete snInt; - close(sigintFd[0]); - close(sigintFd[1]); -#endif - -#ifdef __APPLE__ - bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); - bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); - if (vsyncDisabled && resetVSync) - EnableOSXVSync(true); -#endif - - os_inhibit_sleep_set_active(sleepInhibitor, false); - os_inhibit_sleep_destroy(sleepInhibitor); - - if (libobs_initialized) - obs_shutdown(); -} - -static void move_basic_to_profiles(void) -{ - char path[512]; - - if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { - return; - } - - const std::filesystem::path basicPath = std::filesystem::u8path(path); - - if (!std::filesystem::exists(basicPath)) { - return; - } - - const std::filesystem::path profilesPath = - App()->userProfilesLocation / std::filesystem::u8path("obs-studio/basic/profiles"); - - if (std::filesystem::exists(profilesPath)) { - return; - } - - try { - std::filesystem::create_directories(profilesPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create profiles directory for migration from basic profile\n%s", - error.what()); - return; - } - - const std::filesystem::path newProfilePath = profilesPath / std::filesystem::u8path(Str("Untitled")); - - for (auto &entry : std::filesystem::directory_iterator(basicPath)) { - if (entry.is_directory()) { - continue; - } - - if (entry.path().filename().u8string() == "scenes.json") { - continue; - } - - if (!std::filesystem::exists(newProfilePath)) { - try { - std::filesystem::create_directory(newProfilePath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to create profile directory for 'Untitled'\n%s", error.what()); - return; - } - } - - const filesystem::path destinationFile = newProfilePath / entry.path().filename(); - - const auto copyOptions = std::filesystem::copy_options::overwrite_existing; - - try { - std::filesystem::copy(entry.path(), destinationFile, copyOptions); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s", - entry.path().filename().u8string().c_str(), error.what()); - - return; - } - } -} - -static void move_basic_to_scene_collections(void) -{ - char path[512]; - - if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { - return; - } - - const std::filesystem::path basicPath = std::filesystem::u8path(path); - - if (!std::filesystem::exists(basicPath)) { - return; - } - - const std::filesystem::path sceneCollectionPath = - App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); - - if (std::filesystem::exists(sceneCollectionPath)) { - return; - } - - try { - std::filesystem::create_directories(sceneCollectionPath); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, - "Failed to create scene collection directory for migration from basic scene collection\n%s", - error.what()); - return; - } - - const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); - const std::filesystem::path destinationFile = - (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); - - try { - std::filesystem::rename(sourceFile, destinationFile); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); - return; - } -} - -void OBSApp::AppInit() -{ - ProfileScope("OBSApp::AppInit"); - - if (!MakeUserDirs()) - throw "Failed to create required user directories"; - if (!InitGlobalConfig()) - throw "Failed to initialize global config"; - if (!InitLocale()) - throw "Failed to load locale"; - if (!InitTheme()) - throw "Failed to load theme"; - - config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); - config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); - config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); - - if (!config_has_user_value(userConfig, "Basic", "Profile")) { - config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); - config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); - } - - if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { - config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); - config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); - } - -#ifdef _WIN32 - bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); - if (disableAudioDucking) - DisableAudioDucking(true); -#endif - -#ifdef __APPLE__ - if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) - EnableOSXVSync(false); -#endif - - UpdateHotkeyFocusSetting(false); - - move_basic_to_profiles(); - move_basic_to_scene_collections(); - - if (!MakeUserProfileDirs()) - throw "Failed to create profile directories"; -} - -const char *OBSApp::GetRenderModule() const -{ - const char *renderer = config_get_string(appConfig, "Video", "Renderer"); - - return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; -} - -static bool StartupOBS(const char *locale, profiler_name_store_t *store) -{ - char path[512]; - - if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) - return false; - - return obs_startup(locale, path, store); -} - -inline void OBSApp::ResetHotkeyState(bool inFocus) -{ - obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); -} - -void OBSApp::UpdateHotkeyFocusSetting(bool resetState) -{ - enableHotkeysInFocus = true; - enableHotkeysOutOfFocus = true; - - const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); - - if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { - enableHotkeysInFocus = false; - } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { - enableHotkeysOutOfFocus = false; - } - - if (resetState) - ResetHotkeyState(applicationState() == Qt::ApplicationActive); -} - -void OBSApp::DisableHotkeys() -{ - enableHotkeysInFocus = false; - enableHotkeysOutOfFocus = false; - ResetHotkeyState(applicationState() == Qt::ApplicationActive); -} - -Q_DECLARE_METATYPE(VoidFunc) - -void OBSApp::Exec(VoidFunc func) -{ - func(); -} - -static void ui_task_handler(obs_task_t task, void *param, bool wait) -{ - auto doTask = [=]() { - /* to get clang-format to behave */ - task(param); - }; - QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); -} - -bool OBSApp::OBSInit() -{ - ProfileScope("OBSApp::OBSInit"); - - qRegisterMetaType("VoidFunc"); - -#if !defined(_WIN32) && !defined(__APPLE__) - if (QApplication::platformName() == "xcb") { - obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); - blog(LOG_INFO, "Using EGL/X11"); - } - -#ifdef ENABLE_WAYLAND - if (QApplication::platformName().contains("wayland")) { - obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); - setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); - blog(LOG_INFO, "Platform: Wayland"); - } -#endif - - QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); - obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); -#endif - -#ifdef __APPLE__ - setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); -#endif - - if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) - return false; - - libobs_initialized = true; - - obs_set_ui_task_handler(ui_task_handler); - -#if defined(_WIN32) || defined(__APPLE__) - bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); - obs_apply_private_data(settings); - - blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); - - blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); -#endif -#ifdef _WIN32 - bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); - blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); -#endif - - blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); - blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); - - if (safe_mode) { - blog(LOG_WARNING, "Safe Mode enabled."); - } else if (disable_3p_plugins) { - blog(LOG_WARNING, "Third-party plugins disabled."); - } - - setQuitOnLastWindowClosed(false); - - mainWindow = new OBSBasic(); - - mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); - connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); - - mainWindow->OBSInit(); - - connect(this, &QGuiApplication::applicationStateChanged, - [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); - ResetHotkeyState(applicationState() == Qt::ApplicationActive); - return true; -} - -string OBSApp::GetVersionString(bool platform) const -{ - stringstream ver; - -#ifdef HAVE_OBSCONFIG_H - ver << obs_get_version_string(); -#else - ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; - -#endif - - if (platform) { - ver << " ("; -#ifdef _WIN32 - if (sizeof(void *) == 8) - ver << "64-bit, "; - else - ver << "32-bit, "; - - ver << "windows)"; -#elif __APPLE__ - ver << "mac)"; -#elif __OpenBSD__ - ver << "openbsd)"; -#elif __FreeBSD__ - ver << "freebsd)"; -#else /* assume linux for the time being */ - ver << "linux)"; -#endif - } - - return ver.str(); -} - -bool OBSApp::IsPortableMode() -{ - return portable_mode; -} - -bool OBSApp::IsUpdaterDisabled() -{ - return opt_disable_updater; -} - -bool OBSApp::IsMissingFilesCheckDisabled() -{ - return opt_disable_missing_files_check; -} - -#ifdef __APPLE__ -#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" -#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" -#elif _WIN32 -#define INPUT_AUDIO_SOURCE "wasapi_input_capture" -#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" -#else -#define INPUT_AUDIO_SOURCE "pulse_input_capture" -#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" -#endif - -const char *OBSApp::InputAudioSource() const -{ - return INPUT_AUDIO_SOURCE; -} - -const char *OBSApp::OutputAudioSource() const -{ - return OUTPUT_AUDIO_SOURCE; -} - -const char *OBSApp::GetLastLog() const -{ - return lastLogFile.c_str(); -} - -const char *OBSApp::GetCurrentLog() const -{ - return currentLogFile.c_str(); -} - -const char *OBSApp::GetLastCrashLog() const -{ - return lastCrashLogFile.c_str(); -} - -bool OBSApp::TranslateString(const char *lookupVal, const char **out) const -{ - for (obs_frontend_translate_ui_cb cb : translatorHooks) { - if (cb(lookupVal, out)) - return true; - } - - return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); -} - -// Global handler to receive all QEvent::Show events so we can apply -// display affinity on any newly created windows and dialogs without -// caring where they are coming from (e.g. plugins). -bool OBSApp::notify(QObject *receiver, QEvent *e) -{ - QWidget *w; - QWindow *window; - int windowType; - - if (!receiver->isWidgetType()) - goto skip; - - if (e->type() != QEvent::Show) - goto skip; - - w = qobject_cast(receiver); - - if (!w->isWindow()) - goto skip; - - window = w->windowHandle(); - if (!window) - goto skip; - - windowType = window->flags() & Qt::WindowType::WindowType_Mask; - - if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || - windowType == Qt::WindowType::Tool) { - OBSBasic *main = reinterpret_cast(GetMainWindow()); - if (main) - main->SetDisplayAffinity(window); - } - -skip: - return QApplication::notify(receiver, e); -} +#include "moc_OBSTranslator.cpp" QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const { @@ -1447,1216 +33,3 @@ QString OBSTranslator::translate(const char *, const char *sourceText, const cha return QT_UTF8(out); } - -static bool get_token(lexer *lex, string &str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - str.assign(token.text.array, token.text.len); - return true; -} - -static bool expect_token(lexer *lex, const char *str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - return strref_cmp(&token.text, str) == 0; -} - -static uint64_t convert_log_name(bool has_prefix, const char *name) -{ - BaseLexer lex; - string year, month, day, hour, minute, second; - - lexer_start(lex, name); - - if (has_prefix) { - string temp; - if (!get_token(lex, temp, BASETOKEN_ALPHA)) - return 0; - } - - if (!get_token(lex, year, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, month, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, day, BASETOKEN_DIGIT)) - return 0; - if (!get_token(lex, hour, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, minute, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, second, BASETOKEN_DIGIT)) - return 0; - - stringstream timestring; - timestring << year << month << day << hour << minute << second; - return std::stoull(timestring.str()); -} - -/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ -/* TODO: Remove after version 32.0. */ -#if defined(__FreeBSD__) -static void move_to_xdg(void) -{ - char old_path[512]; - char new_path[512]; - char *home = getenv("HOME"); - if (!home) - return; - - if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) - return; - - /* make base xdg path if it doesn't already exist */ - if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) - return; - if (os_mkdirs(new_path) == MKDIR_ERROR) - return; - - if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) - return; - - if (os_file_exists(old_path) && !os_file_exists(new_path)) { - rename(old_path, new_path); - } -} -#endif - -static void delete_oldest_file(bool has_prefix, const char *location) -{ - BPtr logDir(GetAppConfigPathPtr(location)); - string oldestLog; - uint64_t oldest_ts = (uint64_t)-1; - struct os_dirent *entry; - - unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); - - os_dir_t *dir = os_opendir(logDir); - if (dir) { - unsigned int count = 0; - - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts) { - if (ts < oldest_ts) { - oldestLog = entry->d_name; - oldest_ts = ts; - } - - count++; - } - } - - os_closedir(dir); - - if (count > maxLogs) { - stringstream delPath; - - delPath << logDir << "/" << oldestLog; - os_unlink(delPath.str().c_str()); - } - } -} - -static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) -{ - BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); - struct os_dirent *entry; - os_dir_t *dir = os_opendir(logDir); - uint64_t highest_ts = 0; - - if (dir) { - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts > highest_ts) { - last = entry->d_name; - highest_ts = ts; - } - } - - os_closedir(dir); - } -} - -string GenerateTimeDateFilename(const char *extension, bool noSpace) -{ - time_t now = time(0); - char file[256] = {}; - struct tm *cur_time; - - cur_time = localtime(&now); - snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, - cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, - extension); - - return string(file); -} - -string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) -{ - BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); - return string(filename); -} - -static void FindBestFilename(string &strPath, bool noSpace) -{ - int num = 2; - - if (!os_file_exists(strPath.c_str())) - return; - - const char *ext = strrchr(strPath.c_str(), '.'); - if (!ext) - return; - - int extStart = int(ext - strPath.c_str()); - for (;;) { - string testPath = strPath; - string numStr; - - numStr = noSpace ? "_" : " ("; - numStr += to_string(num++); - if (!noSpace) - numStr += ")"; - - testPath.insert(extStart, numStr); - - if (!os_file_exists(testPath.c_str())) { - strPath = testPath; - break; - } - } -} - -static void ensure_directory_exists(string &path) -{ - replace(path.begin(), path.end(), '\\', '/'); - - size_t last = path.rfind('/'); - if (last == string::npos) - return; - - string directory = path.substr(0, last); - os_mkdirs(directory.c_str()); -} - -static void remove_reserved_file_characters(string &s) -{ - replace(s.begin(), s.end(), '\\', '/'); - replace(s.begin(), s.end(), '*', '_'); - replace(s.begin(), s.end(), '?', '_'); - replace(s.begin(), s.end(), '"', '_'); - replace(s.begin(), s.end(), '|', '_'); - replace(s.begin(), s.end(), ':', '_'); - replace(s.begin(), s.end(), '>', '_'); - replace(s.begin(), s.end(), '<', '_'); -} - -string GetFormatString(const char *format, const char *prefix, const char *suffix) -{ - string f; - - f = format; - - if (prefix && *prefix) { - string str_prefix = prefix; - - if (str_prefix.back() != ' ') - str_prefix += " "; - - size_t insert_pos = 0; - size_t tmp; - - tmp = f.find_last_of('/'); - if (tmp != string::npos && tmp > insert_pos) - insert_pos = tmp + 1; - - tmp = f.find_last_of('\\'); - if (tmp != string::npos && tmp > insert_pos) - insert_pos = tmp + 1; - - f.insert(insert_pos, str_prefix); - } - - if (suffix && *suffix) { - if (*suffix != ' ') - f += " "; - f += suffix; - } - - remove_reserved_file_characters(f); - - return f; -} - -string GetFormatExt(const char *container) -{ - string ext = container; - if (ext == "fragmented_mp4") - ext = "mp4"; - if (ext == "hybrid_mp4") - ext = "mp4"; - else if (ext == "fragmented_mov") - ext = "mov"; - else if (ext == "hls") - ext = "m3u8"; - else if (ext == "mpegts") - ext = "ts"; - - return ext; -} - -string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; - - if (!dir) { - if (main->isVisible()) - OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); - else - main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); - return ""; - } - - os_closedir(dir); - - string strPath; - strPath += path; - - char lastChar = strPath.back(); - if (lastChar != '/' && lastChar != '\\') - strPath += "/"; - - string ext = GetFormatExt(container); - strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); - ensure_directory_exists(strPath); - if (!overwrite) - FindBestFilename(strPath, noSpace); - - return strPath; -} - -vector> GetLocaleNames() -{ - string path; - if (!GetDataFilePath("locale.ini", path)) - throw "Could not find locale.ini path"; - - ConfigFile ini; - if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) - throw "Could not open locale.ini"; - - size_t sections = config_num_sections(ini); - - vector> names; - names.reserve(sections); - for (size_t i = 0; i < sections; i++) { - const char *tag = config_get_section(ini, i); - const char *name = config_get_string(ini, tag, "Name"); - names.emplace_back(tag, name); - } - - return names; -} - -static void create_log_file(fstream &logFile) -{ - stringstream dst; - - get_last_log(false, "obs-studio/logs", lastLogFile); -#ifdef _WIN32 - get_last_log(true, "obs-studio/crashes", lastCrashLogFile); -#endif - - currentLogFile = GenerateTimeDateFilename("txt"); - dst << "obs-studio/logs/" << currentLogFile.c_str(); - - BPtr path(GetAppConfigPathPtr(dst.str().c_str())); - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); -#else - logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); -#endif - - if (logFile.is_open()) { - delete_oldest_file(false, "obs-studio/logs"); - base_set_log_handler(do_log, &logFile); - } else { - blog(LOG_ERROR, "Failed to open log file"); - } -} - -static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { - profiler_name_store_free(store); -}; - -using ProfilerNameStore = std::unique_ptr; - -ProfilerNameStore CreateNameStore() -{ - return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; -} - -static auto SnapshotRelease = [](profiler_snapshot_t *snap) { - profile_snapshot_free(snap); -}; - -using ProfilerSnapshot = std::unique_ptr; - -ProfilerSnapshot GetSnapshot() -{ - return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; -} - -static void SaveProfilerData(const ProfilerSnapshot &snap) -{ - if (currentLogFile.empty()) - return; - - auto pos = currentLogFile.rfind('.'); - if (pos == currentLogFile.npos) - return; - -#define LITERAL_SIZE(x) x, (sizeof(x) - 1) - ostringstream dst; - dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); - dst.write(currentLogFile.c_str(), pos); - dst.write(LITERAL_SIZE(".csv.gz")); -#undef LITERAL_SIZE - - BPtr path = GetAppConfigPathPtr(dst.str().c_str()); - if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) - blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); -} - -static auto ProfilerFree = [](void *) { - profiler_stop(); - - auto snap = GetSnapshot(); - - profiler_print(snap.get()); - profiler_print_time_between_calls(snap.get()); - - SaveProfilerData(snap); - - profiler_free(); -}; - -QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) -{ - if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) - return new VolumeAccessibleInterface(static_cast(object)); - - return nullptr; -} - -static const char *run_program_init = "run_program_init"; -static int run_program(fstream &logFile, int argc, char *argv[]) -{ - int ret = -1; - - auto profilerNameStore = CreateNameStore(); - - std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); - - profiler_start(); - profile_register_root(run_program_init, 0); - - ScopeProfiler prof{run_program_init}; - -#ifdef _WIN32 - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - - QCoreApplication::addLibraryPath("."); - -#if __APPLE__ - InstallNSApplicationSubclass(); - InstallNSThreadLocks(); - - if (!isInBundle()) { - blog(LOG_ERROR, - "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); - return ret; - } -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) - /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and - * crashes loading saved geometry. Just turn off this theme and let users complain OBS - * looks ugly instead of crashing. */ - const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); - if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) - unsetenv("QT_QPA_PLATFORMTHEME"); -#endif - - /* NOTE: This disables an optimisation in Qt that attempts to determine if - * any "siblings" intersect with a widget when determining the approximate - * visible/unobscured area. However, by Qt's own admission this is slow - * and in the case of OBS it significantly slows down lists with many - * elements (e.g. Hotkeys) and it is actually faster to disable it. */ - qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); - - OBSApp program(argc, argv, profilerNameStore.get()); - try { - QAccessible::installFactory(accessibleFactory); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); - - bool created_log = false; - - program.AppInit(); - delete_oldest_file(false, "obs-studio/profiler_data"); - - OBSTranslator translator; - program.installTranslator(&translator); - - /* --------------------------------------- */ - /* check and warn if already running */ - - bool cancel_launch = false; - bool already_running = false; - -#ifdef _WIN32 - RunOnceMutex rom = -#endif - CheckIfAlreadyRunning(already_running); - - if (!already_running) { - goto run; - } - - if (!multi) { - QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), - QTStr("AlreadyRunning.Text")); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); - QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); - mb.setDefaultButton(cancelButton); - - mb.exec(); - cancel_launch = mb.clickedButton() == cancelButton; - } - - if (cancel_launch) - return 0; - - if (!created_log) { - create_log_file(logFile); - created_log = true; - } - - if (multi) { - blog(LOG_INFO, "User enabled --multi flag and is now " - "running multiple instances of OBS."); - } else { - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "Warning: OBS is already running!"); - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "User is now running multiple " - "instances of OBS!"); - /* Clear unclean_shutdown flag as multiple instances - * running from the same config will lead to a - * false-positive detection.*/ - unclean_shutdown = false; - } - - /* --------------------------------------- */ - run: - -#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) - // Mounted by termina during chromeOS linux container startup - // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh - os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); - if (crosDir) { - QMessageBox::StandardButtons buttons(QMessageBox::Ok); - QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, - nullptr); - - mb.exec(); - return 0; - } -#endif - - if (!created_log) - create_log_file(logFile); - - if (unclean_shutdown) { - blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); - } - - if (unclean_shutdown && !safe_mode) { - QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); - QPushButton *launchSafeButton = - mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); - QPushButton *launchNormalButton = - mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); - mb.setDefaultButton(launchNormalButton); - mb.exec(); - - safe_mode = mb.clickedButton() == launchSafeButton; - if (safe_mode) { - blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); - } else { - blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); - } - } - - qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { - switch (type) { -#ifdef _DEBUG - case QtDebugMsg: - blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); - break; - case QtInfoMsg: - blog(LOG_INFO, "%s", QT_TO_UTF8(message)); - break; -#else - case QtDebugMsg: - case QtInfoMsg: - break; -#endif - case QtWarningMsg: - blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); - break; - case QtCriticalMsg: - case QtFatalMsg: - blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); - break; - } - }); - -#ifdef __APPLE__ - MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); - MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); - MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); - MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); - - int permissionsDialogLastShown = - config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); - if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { - OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, - accessibility_permission); - check.exec(); - } -#endif - -#ifdef _WIN32 - if (IsRunningOnWine()) { - QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); - mb.setTextFormat(Qt::RichText); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); - QPushButton *closeButton = mb.addButton(QMessageBox::Close); - mb.setDefaultButton(closeButton); - - mb.exec(); - if (mb.clickedButton() == closeButton) - return 0; - } -#endif - - if (argc > 1) { - stringstream stor; - stor << argv[1]; - for (int i = 2; i < argc; ++i) { - stor << " " << argv[i]; - } - blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); - } - - if (!program.OBSInit()) - return 0; - - prof.Stop(); - - ret = program.exec(); - - } catch (const char *error) { - blog(LOG_ERROR, "%s", error); - OBSErrorBox(nullptr, "%s", error); - } - - if (restart || restart_safe) { - arguments = qApp->arguments(); - - if (restart_safe) { - arguments.append("--safe-mode"); - } else { - arguments.removeAll("--safe-mode"); - } - } - - return ret; -} - -#define MAX_CRASH_REPORT_SIZE (150 * 1024) - -#ifdef _WIN32 - -#define CRASH_MESSAGE \ - "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ - "to the clipboard? The crash log will still be saved to:\n\n%s" - -static void main_crash_handler(const char *format, va_list args, void * /* param */) -{ - char *text = new char[MAX_CRASH_REPORT_SIZE]; - - vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); - text[MAX_CRASH_REPORT_SIZE - 1] = 0; - - string crashFilePath = "obs-studio/crashes"; - - delete_oldest_file(true, crashFilePath.c_str()); - - string name = crashFilePath + "/"; - name += "Crash " + GenerateTimeDateFilename("txt"); - - BPtr path(GetAppConfigPathPtr(name.c_str())); - - fstream file; - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#else - file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#endif - file << text; - file.close(); - - string pathString(path.Get()); - -#ifdef _WIN32 - std::replace(pathString.begin(), pathString.end(), '/', '\\'); -#endif - - string absolutePath = canonical(filesystem::path(pathString)).u8string(); - - size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); - - unique_ptr message_buffer(new char[size + 1]); - - snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); - - string finalMessage = string(message_buffer.get(), message_buffer.get() + size); - - int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); - - if (ret == IDYES) { - size_t len = strlen(text); - - HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); - memcpy(GlobalLock(mem), text, len); - GlobalUnlock(mem); - - OpenClipboard(0); - EmptyClipboard(); - SetClipboardData(CF_TEXT, mem); - CloseClipboard(); - } - - exit(-1); -} - -static void load_debug_privilege(void) -{ - const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; - TOKEN_PRIVILEGES tp; - HANDLE token; - LUID val; - - if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { - return; - } - - if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); - } - - if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { - blog(LOG_INFO, "Could not set privilege to " - "increase GPU priority"); - } - } - - CloseHandle(token); -} -#endif - -#ifdef __APPLE__ -#define BASE_PATH ".." -#else -#define BASE_PATH "../.." -#endif - -#define CONFIG_PATH BASE_PATH "/config" - -#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) -#define ALLOW_PORTABLE_MODE 1 -#else -#define ALLOW_PORTABLE_MODE 0 -#endif - -int GetAppConfigPath(char *path, size_t size, const char *name) -{ -#if ALLOW_PORTABLE_MODE - if (portable_mode) { - if (name && *name) { - return snprintf(path, size, CONFIG_PATH "/%s", name); - } else { - return snprintf(path, size, CONFIG_PATH); - } - } else { - return os_get_config_path(path, size, name); - } -#else - return os_get_config_path(path, size, name); -#endif -} - -char *GetAppConfigPathPtr(const char *name) -{ -#if ALLOW_PORTABLE_MODE - if (portable_mode) { - char path[512]; - - if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { - return bstrdup(path); - } else { - return NULL; - } - } else { - return os_get_config_path_ptr(name); - } -#else - return os_get_config_path_ptr(name); -#endif -} - -int GetProgramDataPath(char *path, size_t size, const char *name) -{ - return os_get_program_data_path(path, size, name); -} - -char *GetProgramDataPathPtr(const char *name) -{ - return os_get_program_data_path_ptr(name); -} - -bool GetFileSafeName(const char *name, std::string &file) -{ - size_t base_len = strlen(name); - size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); - std::wstring wfile; - - if (!len) - return false; - - wfile.resize(len); - os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); - - for (size_t i = wfile.size(); i > 0; i--) { - size_t im1 = i - 1; - - if (iswspace(wfile[im1])) { - wfile[im1] = '_'; - } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { - wfile.erase(im1, 1); - } - } - - if (wfile.size() == 0) - wfile = L"characters_only"; - - len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); - if (!len) - return false; - - file.resize(len); - os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); - return true; -} - -bool GetClosestUnusedFileName(std::string &path, const char *extension) -{ - size_t len = path.size(); - if (extension) { - path += "."; - path += extension; - } - - if (!os_file_exists(path.c_str())) - return true; - - int index = 1; - - do { - path.resize(len); - path += std::to_string(++index); - if (extension) { - path += "."; - path += extension; - } - } while (os_file_exists(path.c_str())); - - return true; -} - -bool WindowPositionValid(QRect rect) -{ - for (QScreen *screen : QGuiApplication::screens()) { - if (screen->availableGeometry().intersects(rect)) - return true; - } - return false; -} - -static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) -{ - return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); -} - -static void check_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - /* Safe Mode detection is disabled in Debug builds to keep developers - * somewhat sane. */ - return; -#else - if (disable_shutdown_check) - return; - - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - if (os_file_exists(sentinelPath)) { - unclean_shutdown = true; - return; - } - - os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); -#endif -} - -static void delete_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - return; -#else - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - os_unlink(sentinelPath); -#endif -} - -#ifndef _WIN32 -void OBSApp::SigIntSignalHandler(int s) -{ - /* Handles SIGINT and writes to a socket. Qt will read - * from the socket in the main thread event loop and trigger - * a call to the ProcessSigInt slot, where we can safely run - * shutdown code without signal safety issues. */ - UNUSED_PARAMETER(s); - - char a = 1; - send(sigintFd[0], &a, sizeof(a), 0); -} -#endif - -void OBSApp::ProcessSigInt(void) -{ - /* This looks weird, but we can't ifdef a Qt slot function so - * the SIGINT handler simply does nothing on Windows. */ -#ifndef _WIN32 - char tmp; - recv(sigintFd[1], &tmp, sizeof(tmp), 0); - - OBSBasic *main = reinterpret_cast(GetMainWindow()); - if (main) - main->close(); -#endif -} - -#ifdef _WIN32 -void OBSApp::commitData(QSessionManager &manager) -{ - if (auto main = App()->GetMainWindow()) { - QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); - manager.cancel(); - } -} -#endif - -#ifdef _WIN32 -static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; -static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " - "Redistributables.\n\nYou will now be directed to the download page."; -static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; - -static bool vc_runtime_outdated() -{ - win_version_info ver; - if (!get_dll_ver(L"msvcp140.dll", &ver)) - return true; - /* Major is always 14 (hence 140.dll), so we only care about minor. */ - if (ver.minor >= 40) - return false; - - int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); - if (choice == IDOK) { - /* Open the URL in the default browser. */ - ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); - } - - return true; -} -#endif - -int main(int argc, char *argv[]) -{ -#ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); - - struct sigaction sig_handler; - - sig_handler.sa_handler = OBSApp::SigIntSignalHandler; - sigemptyset(&sig_handler.sa_mask); - sig_handler.sa_flags = 0; - - sigaction(SIGINT, &sig_handler, NULL); - - /* Block SIGPIPE in all threads, this can happen if a thread calls write on - a closed pipe. */ - sigset_t sigpipe_mask; - sigemptyset(&sigpipe_mask); - sigaddset(&sigpipe_mask, SIGPIPE); - sigset_t saved_mask; - if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { - perror("pthread_sigmask"); - exit(1); - } -#endif - -#ifdef _WIN32 - // Abort as early as possible if MSVC runtime is outdated - if (vc_runtime_outdated()) - return 1; - // Try to keep this as early as possible - install_dll_blocklist_hook(); - - obs_init_win32_crash_handler(); - SetErrorMode(SEM_FAILCRITICALERRORS); - load_debug_privilege(); - base_set_crash_handler(main_crash_handler, nullptr); - - const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); - PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); - func(); - } -#endif - - base_get_log_handler(&def_log_handler, nullptr); - -#if defined(__FreeBSD__) - move_to_xdg(); -#endif - - obs_set_cmdline_args(argc, argv); - - for (int i = 1; i < argc; i++) { - if (arg_is(argv[i], "--multi", "-m")) { - multi = true; - disable_shutdown_check = true; - -#if ALLOW_PORTABLE_MODE - } else if (arg_is(argv[i], "--portable", "-p")) { - portable_mode = true; - -#endif - } else if (arg_is(argv[i], "--verbose", nullptr)) { - log_verbose = true; - - } else if (arg_is(argv[i], "--safe-mode", nullptr)) { - safe_mode = true; - - } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { - disable_3p_plugins = true; - - } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { - /* This exists mostly to bypass the dialog during development. */ - disable_shutdown_check = true; - - } else if (arg_is(argv[i], "--always-on-top", nullptr)) { - opt_always_on_top = true; - - } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { - unfiltered_log = true; - - } else if (arg_is(argv[i], "--startstreaming", nullptr)) { - opt_start_streaming = true; - - } else if (arg_is(argv[i], "--startrecording", nullptr)) { - opt_start_recording = true; - - } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { - opt_start_replaybuffer = true; - - } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { - opt_start_virtualcam = true; - - } else if (arg_is(argv[i], "--collection", nullptr)) { - if (++i < argc) - opt_starting_collection = argv[i]; - - } else if (arg_is(argv[i], "--profile", nullptr)) { - if (++i < argc) - opt_starting_profile = argv[i]; - - } else if (arg_is(argv[i], "--scene", nullptr)) { - if (++i < argc) - opt_starting_scene = argv[i]; - - } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { - opt_minimize_tray = true; - - } else if (arg_is(argv[i], "--studio-mode", nullptr)) { - opt_studio_mode = true; - - } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { - opt_allow_opengl = true; - - } else if (arg_is(argv[i], "--disable-updater", nullptr)) { - opt_disable_updater = true; - - } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { - opt_disable_missing_files_check = true; - - } else if (arg_is(argv[i], "--steam", nullptr)) { - steam = true; - - } else if (arg_is(argv[i], "--help", "-h")) { - std::string help = - "--help, -h: Get list of available commands.\n\n" - "--startstreaming: Automatically start streaming.\n" - "--startrecording: Automatically start recording.\n" - "--startreplaybuffer: Start replay buffer.\n" - "--startvirtualcam: Start virtual camera (if available).\n\n" - "--collection : Use specific scene collection." - "\n" - "--profile : Use specific profile.\n" - "--scene : Start with specific scene.\n\n" - "--studio-mode: Enable studio mode.\n" - "--minimize-to-tray: Minimize to system tray.\n" -#if ALLOW_PORTABLE_MODE - "--portable, -p: Use portable mode.\n" -#endif - "--multi, -m: Don't warn when launching multiple instances.\n\n" - "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" - "--only-bundled-plugins: Only load included (first-party) plugins\n" - "--disable-shutdown-check: Disable unclean shutdown detection.\n" - "--verbose: Make log more verbose.\n" - "--always-on-top: Start in 'always on top' mode.\n\n" - "--unfiltered_log: Make log unfiltered.\n\n" - "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" - "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; - -#ifdef _WIN32 - MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); -#else - std::cout << help << "--version, -V: Get current version.\n"; -#endif - exit(0); - - } else if (arg_is(argv[i], "--version", "-V")) { - std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; - exit(0); - } - } - -#if ALLOW_PORTABLE_MODE - if (!portable_mode) { - portable_mode = os_file_exists(BASE_PATH "/portable_mode") || - os_file_exists(BASE_PATH "/obs_portable_mode") || - os_file_exists(BASE_PATH "/portable_mode.txt") || - os_file_exists(BASE_PATH "/obs_portable_mode.txt"); - } - - if (!opt_disable_updater) { - opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || - os_file_exists(BASE_PATH "/disable_updater.txt"); - } - - if (!opt_disable_missing_files_check) { - opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || - os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); - } -#endif - - check_safe_mode_sentinel(); - - fstream logFile; - - curl_global_init(CURL_GLOBAL_ALL); - int ret = run_program(logFile, argc, argv); - -#ifdef _WIN32 - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); - PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); - func(); - FreeLibrary(hRtwq); - } - - log_blocked_dlls(); -#endif - - delete_safe_mode_sentinel(); - blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); - base_set_log_handler(nullptr, nullptr); - - if (restart || restart_safe) { - auto executable = arguments.takeFirst(); - QProcess::startDetached(executable, arguments); - } - - return ret; -} diff --git a/frontend/utility/OBSTranslator.hpp b/frontend/utility/OBSTranslator.hpp index dea5e530b..a962306f3 100644 --- a/frontend/utility/OBSTranslator.hpp +++ b/frontend/utility/OBSTranslator.hpp @@ -17,50 +17,8 @@ #pragma once -#include +#include #include -#include -#include - -#ifndef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "window-main.hpp" -#include "obs-app-theming.hpp" - -std::string CurrentTimeString(); -std::string CurrentDateTimeString(); -std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); -std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); -std::string GetFormatString(const char *format, const char *prefix, const char *suffix); -std::string GetFormatExt(const char *container); -std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, - const char *format); -QObject *CreateShortcutFilter(); - -struct BaseLexer { - lexer lex; - -public: - inline BaseLexer() { lexer_init(&lex); } - inline ~BaseLexer() { lexer_free(&lex); } - operator lexer *() { return &lex; } -}; class OBSTranslator : public QTranslator { Q_OBJECT @@ -71,210 +29,3 @@ public: virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, int n) const override; }; - -typedef std::function VoidFunc; - -struct UpdateBranch { - QString name; - QString display_name; - QString description; - bool is_enabled; - bool is_visible; -}; - -class OBSApp : public QApplication { - Q_OBJECT - -private: - std::string locale; - - ConfigFile appConfig; - ConfigFile userConfig; - TextLookup textLookup; - QPointer mainWindow; - profiler_name_store_t *profilerNameStore = nullptr; - std::vector updateBranches; - bool branches_loaded = false; - - bool libobs_initialized = false; - - os_inhibit_t *sleepInhibitor = nullptr; - int sleepInhibitRefs = 0; - - bool enableHotkeysInFocus = true; - bool enableHotkeysOutOfFocus = true; - - std::deque translatorHooks; - - bool UpdatePre22MultiviewLayout(const char *layout); - - bool InitGlobalConfig(); - bool InitGlobalConfigDefaults(); - bool InitGlobalLocationDefaults(); - - bool MigrateGlobalSettings(); - void MigrateLegacySettings(uint32_t lastVersion); - - bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); - void InitUserConfigDefaults(); - - bool InitLocale(); - bool InitTheme(); - - inline void ResetHotkeyState(bool inFocus); - - QPalette defaultPalette; - OBSTheme *currentTheme = nullptr; - QHash themes; - QPointer themeWatcher; - - void FindThemes(); - - bool notify(QObject *receiver, QEvent *e) override; - -#ifndef _WIN32 - static int sigintFd[2]; - QSocketNotifier *snInt = nullptr; -#else -private slots: - void commitData(QSessionManager &manager); -#endif - -private slots: - void themeFileChanged(const QString &); - -public: - OBSApp(int &argc, char **argv, profiler_name_store_t *store); - ~OBSApp(); - - void AppInit(); - bool OBSInit(); - - void UpdateHotkeyFocusSetting(bool reset = true); - void DisableHotkeys(); - - inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } - - inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } - - inline config_t *GetAppConfig() const { return appConfig; } - inline config_t *GetUserConfig() const { return userConfig; } - std::filesystem::path userConfigLocation; - std::filesystem::path userScenesLocation; - std::filesystem::path userProfilesLocation; - - inline const char *GetLocale() const { return locale.c_str(); } - - OBSTheme *GetTheme() const { return currentTheme; } - QList GetThemes() const { return themes.values(); } - OBSTheme *GetTheme(const QString &name); - bool SetTheme(const QString &name); - bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } - - void SetBranchData(const std::string &data); - std::vector GetBranches(); - - inline lookup_t *GetTextLookup() const { return textLookup; } - - inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } - - bool TranslateString(const char *lookupVal, const char **out) const; - - profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } - - const char *GetLastLog() const; - const char *GetCurrentLog() const; - - const char *GetLastCrashLog() const; - - std::string GetVersionString(bool platform = true) const; - bool IsPortableMode(); - bool IsUpdaterDisabled(); - bool IsMissingFilesCheckDisabled(); - - const char *InputAudioSource() const; - const char *OutputAudioSource() const; - - const char *GetRenderModule() const; - - inline void IncrementSleepInhibition() - { - if (!sleepInhibitor) - return; - if (sleepInhibitRefs++ == 0) - os_inhibit_sleep_set_active(sleepInhibitor, true); - } - - inline void DecrementSleepInhibition() - { - if (!sleepInhibitor) - return; - if (sleepInhibitRefs == 0) - return; - if (--sleepInhibitRefs == 0) - os_inhibit_sleep_set_active(sleepInhibitor, false); - } - - inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } - - inline void PopUITranslation() { translatorHooks.pop_front(); } -#ifndef _WIN32 - static void SigIntSignalHandler(int); -#endif - -public slots: - void Exec(VoidFunc func); - void ProcessSigInt(); - -signals: - void StyleChanged(); -}; - -int GetAppConfigPath(char *path, size_t size, const char *name); -char *GetAppConfigPathPtr(const char *name); - -int GetProgramDataPath(char *path, size_t size, const char *name); -char *GetProgramDataPathPtr(const char *name); - -inline OBSApp *App() -{ - return static_cast(qApp); -} - -std::vector> GetLocaleNames(); -inline const char *Str(const char *lookup) -{ - return App()->GetString(lookup); -} -inline QString QTStr(const char *lookupVal) -{ - return QString::fromUtf8(Str(lookupVal)); -} - -bool GetFileSafeName(const char *name, std::string &file); -bool GetClosestUnusedFileName(std::string &path, const char *extension); -bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); - -bool WindowPositionValid(QRect rect); - -extern bool portable_mode; -extern bool steam; -extern bool safe_mode; -extern bool disable_3p_plugins; - -extern bool opt_start_streaming; -extern bool opt_start_recording; -extern bool opt_start_replaybuffer; -extern bool opt_start_virtualcam; -extern bool opt_minimize_tray; -extern bool opt_studio_mode; -extern bool opt_allow_opengl; -extern bool opt_always_on_top; -extern std::string opt_starting_scene; -extern bool restart; -extern bool restart_safe; - -#ifdef _WIN32 -extern "C" void install_dll_blocklist_hook(void); -extern "C" void log_blocked_dlls(void); -#endif From ff3b62422e414946a696e3be134aa5f6849be799 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:10:40 +0100 Subject: [PATCH 28/37] frontend: Migrate Windows updater --- {UI/win-update => frontend}/updater/CMakeLists.txt | 2 +- {UI/win-update => frontend}/updater/hash.cpp | 0 {UI/win-update => frontend}/updater/helpers.cpp | 0 {UI/win-update => frontend}/updater/helpers.hpp | 0 {UI/win-update => frontend}/updater/http.cpp | 0 {UI/win-update => frontend}/updater/init-hook-files.c | 0 {UI/win-update => frontend}/updater/manifest.hpp | 0 {UI/win-update => frontend}/updater/patch.cpp | 0 {UI/win-update => frontend}/updater/resource.h | 0 {UI/win-update => frontend}/updater/updater.cpp | 0 {UI/win-update => frontend}/updater/updater.hpp | 0 {UI/win-update => frontend}/updater/updater.manifest | 0 {UI/win-update => frontend}/updater/updater.rc | 0 13 files changed, 1 insertion(+), 1 deletion(-) rename {UI/win-update => frontend}/updater/CMakeLists.txt (95%) rename {UI/win-update => frontend}/updater/hash.cpp (100%) rename {UI/win-update => frontend}/updater/helpers.cpp (100%) rename {UI/win-update => frontend}/updater/helpers.hpp (100%) rename {UI/win-update => frontend}/updater/http.cpp (100%) rename {UI/win-update => frontend}/updater/init-hook-files.c (100%) rename {UI/win-update => frontend}/updater/manifest.hpp (100%) rename {UI/win-update => frontend}/updater/patch.cpp (100%) rename {UI/win-update => frontend}/updater/resource.h (100%) rename {UI/win-update => frontend}/updater/updater.cpp (100%) rename {UI/win-update => frontend}/updater/updater.hpp (100%) rename {UI/win-update => frontend}/updater/updater.manifest (100%) rename {UI/win-update => frontend}/updater/updater.rc (100%) diff --git a/UI/win-update/updater/CMakeLists.txt b/frontend/updater/CMakeLists.txt similarity index 95% rename from UI/win-update/updater/CMakeLists.txt rename to frontend/updater/CMakeLists.txt index 443509ff3..abaa84c76 100644 --- a/UI/win-update/updater/CMakeLists.txt +++ b/frontend/updater/CMakeLists.txt @@ -24,7 +24,7 @@ target_sources( target_compile_definitions(updater PRIVATE NOMINMAX "PSAPI_VERSION=2") -target_include_directories(updater PRIVATE "${CMAKE_SOURCE_DIR}/libobs" "${CMAKE_SOURCE_DIR}/UI/win-update") +target_include_directories(updater PRIVATE "${CMAKE_SOURCE_DIR}/libobs" "${CMAKE_SOURCE_DIR}/frontend/utility") target_link_libraries( updater diff --git a/UI/win-update/updater/hash.cpp b/frontend/updater/hash.cpp similarity index 100% rename from UI/win-update/updater/hash.cpp rename to frontend/updater/hash.cpp diff --git a/UI/win-update/updater/helpers.cpp b/frontend/updater/helpers.cpp similarity index 100% rename from UI/win-update/updater/helpers.cpp rename to frontend/updater/helpers.cpp diff --git a/UI/win-update/updater/helpers.hpp b/frontend/updater/helpers.hpp similarity index 100% rename from UI/win-update/updater/helpers.hpp rename to frontend/updater/helpers.hpp diff --git a/UI/win-update/updater/http.cpp b/frontend/updater/http.cpp similarity index 100% rename from UI/win-update/updater/http.cpp rename to frontend/updater/http.cpp diff --git a/UI/win-update/updater/init-hook-files.c b/frontend/updater/init-hook-files.c similarity index 100% rename from UI/win-update/updater/init-hook-files.c rename to frontend/updater/init-hook-files.c diff --git a/UI/win-update/updater/manifest.hpp b/frontend/updater/manifest.hpp similarity index 100% rename from UI/win-update/updater/manifest.hpp rename to frontend/updater/manifest.hpp diff --git a/UI/win-update/updater/patch.cpp b/frontend/updater/patch.cpp similarity index 100% rename from UI/win-update/updater/patch.cpp rename to frontend/updater/patch.cpp diff --git a/UI/win-update/updater/resource.h b/frontend/updater/resource.h similarity index 100% rename from UI/win-update/updater/resource.h rename to frontend/updater/resource.h diff --git a/UI/win-update/updater/updater.cpp b/frontend/updater/updater.cpp similarity index 100% rename from UI/win-update/updater/updater.cpp rename to frontend/updater/updater.cpp diff --git a/UI/win-update/updater/updater.hpp b/frontend/updater/updater.hpp similarity index 100% rename from UI/win-update/updater/updater.hpp rename to frontend/updater/updater.hpp diff --git a/UI/win-update/updater/updater.manifest b/frontend/updater/updater.manifest similarity index 100% rename from UI/win-update/updater/updater.manifest rename to frontend/updater/updater.manifest diff --git a/UI/win-update/updater/updater.rc b/frontend/updater/updater.rc similarity index 100% rename from UI/win-update/updater/updater.rc rename to frontend/updater/updater.rc From 08a2eb304b47fe6dba57b7e3675ca699f765a4be Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:13:03 +0100 Subject: [PATCH 29/37] frontend: Migrate CMake files --- UI/cmake/feature-browserpanels.cmake | 10 -- UI/cmake/feature-importers.cmake | 10 -- UI/cmake/ui-elements.cmake | 78 --------------- UI/cmake/ui-qt.cmake | 65 ------------- UI/cmake/ui-windows.cmake | 63 ------------ UI/obs.manifest | 20 ---- UI/obs.rc.in | 26 ----- {UI => frontend}/CMakeLists.txt | 91 +++++++----------- frontend/cmake/feature-browserpanels.cmake | 18 ++++ frontend/cmake/feature-importers.cmake | 16 +++ .../cmake/feature-macos-update.cmake | 18 ++-- {UI => frontend}/cmake/feature-restream.cmake | 2 +- {UI => frontend}/cmake/feature-sparkle.cmake | 13 ++- {UI => frontend}/cmake/feature-twitch.cmake | 2 +- {UI => frontend}/cmake/feature-whatsnew.cmake | 16 +-- {UI => frontend}/cmake/feature-youtube.cmake | 19 ++-- .../cmake/linux/com.obsproject.Studio.desktop | 0 .../com.obsproject.Studio.metainfo.xml.in | 0 .../cmake/linux/icons/obs-logo-128.png | Bin .../cmake/linux/icons/obs-logo-256.png | Bin .../cmake/linux/icons/obs-logo-512.png | Bin .../cmake/linux/icons/obs-logo-scalable.svg | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/icon_128x128.png | Bin .../AppIcon.appiconset/icon_128x128@2x.png | Bin .../AppIcon.appiconset/icon_16x16.png | Bin .../AppIcon.appiconset/icon_16x16@2x.png | Bin .../AppIcon.appiconset/icon_256x256.png | Bin .../AppIcon.appiconset/icon_256x256@2x.png | Bin .../AppIcon.appiconset/icon_32x32.png | Bin .../AppIcon.appiconset/icon_32x32@2x.png | Bin .../AppIcon.appiconset/icon_512x512.png | Bin .../AppIcon.appiconset/icon_512x512@2x.png | Bin .../cmake/macos/Assets.xcassets/Contents.json | 0 {UI => frontend}/cmake/macos/Info.plist.in | 0 .../cmake/macos/entitlements-extension.plist | 0 .../cmake/macos/entitlements.plist | 0 .../macos/exportOptions-extension.plist.in | 0 .../cmake/macos/exportOptions.plist.in | 0 {UI => frontend}/cmake/macos/qt.conf | 0 {UI => frontend}/cmake/os-freebsd.cmake | 4 +- {UI => frontend}/cmake/os-linux.cmake | 4 +- {UI => frontend}/cmake/os-macos.cmake | 12 ++- {UI => frontend}/cmake/os-windows.cmake | 58 ++++++----- .../cmake/templates}/ui-config.h.in | 0 frontend/cmake/ui-components.cmake | 85 ++++++++++++++++ frontend/cmake/ui-dialogs.cmake | 40 ++++++++ frontend/cmake/ui-docks.cmake | 1 + frontend/cmake/ui-oauth.cmake | 4 + frontend/cmake/ui-qt.cmake | 58 +++++++++++ frontend/cmake/ui-settings.cmake | 15 +++ frontend/cmake/ui-utility.cmake | 69 +++++++++++++ frontend/cmake/ui-widgets.cmake | 66 +++++++++++++ frontend/cmake/ui-wizards.cmake | 15 +++ {UI => frontend}/cmake/windows/obs-studio.ico | Bin {UI => frontend}/cmake/windows/obs.manifest | 0 {UI => frontend}/cmake/windows/obs.rc.in | 0 57 files changed, 509 insertions(+), 389 deletions(-) delete mode 100644 UI/cmake/feature-browserpanels.cmake delete mode 100644 UI/cmake/feature-importers.cmake delete mode 100644 UI/cmake/ui-elements.cmake delete mode 100644 UI/cmake/ui-qt.cmake delete mode 100644 UI/cmake/ui-windows.cmake delete mode 100644 UI/obs.manifest delete mode 100644 UI/obs.rc.in rename {UI => frontend}/CMakeLists.txt (55%) create mode 100644 frontend/cmake/feature-browserpanels.cmake create mode 100644 frontend/cmake/feature-importers.cmake rename {UI => frontend}/cmake/feature-macos-update.cmake (51%) rename {UI => frontend}/cmake/feature-restream.cmake (78%) rename {UI => frontend}/cmake/feature-sparkle.cmake (65%) rename {UI => frontend}/cmake/feature-twitch.cmake (78%) rename {UI => frontend}/cmake/feature-whatsnew.cmake (68%) rename {UI => frontend}/cmake/feature-youtube.cmake (55%) rename {UI => frontend}/cmake/linux/com.obsproject.Studio.desktop (100%) rename {UI => frontend}/cmake/linux/com.obsproject.Studio.metainfo.xml.in (100%) rename {UI => frontend}/cmake/linux/icons/obs-logo-128.png (100%) rename {UI => frontend}/cmake/linux/icons/obs-logo-256.png (100%) rename {UI => frontend}/cmake/linux/icons/obs-logo-512.png (100%) rename {UI => frontend}/cmake/linux/icons/obs-logo-scalable.svg (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png (100%) rename {UI => frontend}/cmake/macos/Assets.xcassets/Contents.json (100%) rename {UI => frontend}/cmake/macos/Info.plist.in (100%) rename {UI => frontend}/cmake/macos/entitlements-extension.plist (100%) rename {UI => frontend}/cmake/macos/entitlements.plist (100%) rename {UI => frontend}/cmake/macos/exportOptions-extension.plist.in (100%) rename {UI => frontend}/cmake/macos/exportOptions.plist.in (100%) rename {UI => frontend}/cmake/macos/qt.conf (100%) rename {UI => frontend}/cmake/os-freebsd.cmake (92%) rename {UI => frontend}/cmake/os-linux.cmake (95%) rename {UI => frontend}/cmake/os-macos.cmake (75%) rename {UI => frontend}/cmake/os-windows.cmake (57%) rename {UI => frontend/cmake/templates}/ui-config.h.in (100%) create mode 100644 frontend/cmake/ui-components.cmake create mode 100644 frontend/cmake/ui-dialogs.cmake create mode 100644 frontend/cmake/ui-docks.cmake create mode 100644 frontend/cmake/ui-oauth.cmake create mode 100644 frontend/cmake/ui-qt.cmake create mode 100644 frontend/cmake/ui-settings.cmake create mode 100644 frontend/cmake/ui-utility.cmake create mode 100644 frontend/cmake/ui-widgets.cmake create mode 100644 frontend/cmake/ui-wizards.cmake rename {UI => frontend}/cmake/windows/obs-studio.ico (100%) rename {UI => frontend}/cmake/windows/obs.manifest (100%) rename {UI => frontend}/cmake/windows/obs.rc.in (100%) diff --git a/UI/cmake/feature-browserpanels.cmake b/UI/cmake/feature-browserpanels.cmake deleted file mode 100644 index 73ef6d2e4..000000000 --- a/UI/cmake/feature-browserpanels.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(TARGET OBS::browser-panels) - target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE) - - target_link_libraries(obs-studio PRIVATE OBS::browser-panels) - - target_sources( - obs-studio - PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp - ) -endif() diff --git a/UI/cmake/feature-importers.cmake b/UI/cmake/feature-importers.cmake deleted file mode 100644 index d11ab27a2..000000000 --- a/UI/cmake/feature-importers.cmake +++ /dev/null @@ -1,10 +0,0 @@ -target_sources( - obs-studio - PRIVATE - importers/classic.cpp - importers/importers.cpp - importers/importers.hpp - importers/sl.cpp - importers/studio.cpp - importers/xsplit.cpp -) diff --git a/UI/cmake/ui-elements.cmake b/UI/cmake/ui-elements.cmake deleted file mode 100644 index d68890945..000000000 --- a/UI/cmake/ui-elements.cmake +++ /dev/null @@ -1,78 +0,0 @@ -if(NOT TARGET OBS::properties-view) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view") -endif() - -if(NOT TARGET OBS::qt-plain-text-edit) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/plain-text-edit" "${CMAKE_BINARY_DIR}/shared/qt/plain-text-edit") -endif() - -if(NOT TARGET OBS::qt-slider-ignorewheel) - add_subdirectory( - "${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel" - "${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel" - ) -endif() - -if(NOT TARGET OBS::qt-vertical-scroll-area) - add_subdirectory( - "${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area" - "${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area" - ) -endif() - -target_link_libraries( - obs-studio - PRIVATE OBS::properties-view OBS::qt-plain-text-edit OBS::qt-slider-ignorewheel OBS::qt-vertical-scroll-area -) - -target_sources( - obs-studio - PRIVATE - absolute-slider.cpp - absolute-slider.hpp - adv-audio-control.cpp - adv-audio-control.hpp - audio-encoders.cpp - audio-encoders.hpp - balance-slider.hpp - basic-controls.cpp - basic-controls.hpp - clickable-label.hpp - context-bar-controls.cpp - context-bar-controls.hpp - focus-list.cpp - focus-list.hpp - horizontal-scroll-area.cpp - horizontal-scroll-area.hpp - hotkey-edit.cpp - hotkey-edit.hpp - item-widget-helpers.cpp - item-widget-helpers.hpp - log-viewer.cpp - log-viewer.hpp - media-controls.cpp - media-controls.hpp - menu-button.cpp - menu-button.hpp - mute-checkbox.hpp - noncheckable-button.hpp - preview-controls.cpp - preview-controls.hpp - remote-text.cpp - remote-text.hpp - scene-tree.cpp - scene-tree.hpp - screenshot-obj.hpp - source-label.cpp - source-label.hpp - source-tree.cpp - source-tree.hpp - undo-stack-obs.cpp - undo-stack-obs.hpp - url-push-button.cpp - url-push-button.hpp - visibility-item-widget.cpp - visibility-item-widget.hpp - volume-control.cpp - volume-control.hpp -) diff --git a/UI/cmake/ui-qt.cmake b/UI/cmake/ui-qt.cmake deleted file mode 100644 index 53d53671a..000000000 --- a/UI/cmake/ui-qt.cmake +++ /dev/null @@ -1,65 +0,0 @@ -find_package(Qt6 REQUIRED Widgets Network Svg Xml) - -if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) - find_package(Qt6 REQUIRED Gui DBus) -endif() - -if(NOT TARGET OBS::qt-wrappers) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers") -endif() - -target_link_libraries( - obs-studio - PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers -) - -set_target_properties( - obs-studio - PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON -) - -set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar) - -set( - _qt_sources - forms/AutoConfigFinishPage.ui - forms/AutoConfigStartPage.ui - forms/AutoConfigStartPage.ui - forms/AutoConfigStreamPage.ui - forms/AutoConfigTestPage.ui - forms/AutoConfigVideoPage.ui - forms/ColorSelect.ui - forms/obs.qrc - forms/OBSAbout.ui - forms/OBSAdvAudio.ui - forms/OBSBasic.ui - forms/OBSBasicControls.ui - forms/OBSBasicFilters.ui - forms/OBSBasicInteraction.ui - forms/OBSBasicProperties.ui - forms/OBSBasicSettings.ui - forms/OBSBasicSourceSelect.ui - forms/OBSBasicTransform.ui - forms/OBSBasicVCamConfig.ui - forms/OBSExtraBrowsers.ui - forms/OBSImporter.ui - forms/OBSLogReply.ui - forms/OBSLogViewer.ui - forms/OBSMissingFiles.ui - forms/OBSRemux.ui - forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui - forms/source-toolbar/browser-source-toolbar.ui - forms/source-toolbar/color-source-toolbar.ui - forms/source-toolbar/device-select-toolbar.ui - forms/source-toolbar/game-capture-toolbar.ui - forms/source-toolbar/image-source-toolbar.ui - forms/source-toolbar/media-controls.ui - forms/source-toolbar/text-source-toolbar.ui -) - -target_sources(obs-studio PRIVATE ${_qt_sources}) - -source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/forms" PREFIX "UI Files" FILES ${_qt_sources}) - -unset(_qt_sources) diff --git a/UI/cmake/ui-windows.cmake b/UI/cmake/ui-windows.cmake deleted file mode 100644 index 84f561edb..000000000 --- a/UI/cmake/ui-windows.cmake +++ /dev/null @@ -1,63 +0,0 @@ -target_sources( - obs-studio - PRIVATE - window-basic-about.cpp - window-basic-about.hpp - window-basic-adv-audio.cpp - window-basic-adv-audio.hpp - window-basic-auto-config-test.cpp - window-basic-auto-config.cpp - window-basic-auto-config.hpp - window-basic-filters.cpp - window-basic-filters.hpp - window-basic-interaction.cpp - window-basic-interaction.hpp - window-basic-main-browser.cpp - window-basic-main-dropfiles.cpp - window-basic-main-icons.cpp - window-basic-main-outputs.cpp - window-basic-main-outputs.hpp - window-basic-main-profiles.cpp - window-basic-main-scene-collections.cpp - window-basic-main-screenshot.cpp - window-basic-main-transitions.cpp - window-basic-main.cpp - window-basic-main.hpp - window-basic-preview.cpp - window-basic-preview.hpp - window-basic-properties.cpp - window-basic-properties.hpp - window-basic-settings-a11y.cpp - window-basic-settings-appearance.cpp - window-basic-settings-stream.cpp - window-basic-settings.cpp - window-basic-settings.hpp - window-basic-source-select.cpp - window-basic-source-select.hpp - window-basic-stats.cpp - window-basic-stats.hpp - window-basic-status-bar.cpp - window-basic-status-bar.hpp - window-basic-transform.cpp - window-basic-transform.hpp - window-basic-vcam-config.cpp - window-basic-vcam-config.hpp - window-basic-vcam.hpp - window-dock.cpp - window-dock.hpp - window-importer.cpp - window-importer.hpp - window-log-reply.cpp - window-log-reply.hpp - window-main.hpp - window-missing-files.cpp - window-missing-files.hpp - window-namedialog.cpp - window-namedialog.hpp - window-projector.cpp - window-projector.hpp - window-remux.cpp - window-remux.hpp - window-whats-new.cpp - window-whats-new.hpp -) diff --git a/UI/obs.manifest b/UI/obs.manifest deleted file mode 100644 index b20d07746..000000000 --- a/UI/obs.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - OBS Studio - - - - - - - - - - - - - - diff --git a/UI/obs.rc.in b/UI/obs.rc.in deleted file mode 100644 index f1f06b697..000000000 --- a/UI/obs.rc.in +++ /dev/null @@ -1,26 +0,0 @@ -IDI_ICON1 ICON DISCARDABLE "../cmake/bundle/windows/obs-studio.ico" - -1 VERSIONINFO -FILEVERSION ${UI_VERSION_MAJOR},${UI_VERSION_MINOR},${UI_VERSION_PATCH},0 -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904B0" - BEGIN - VALUE "CompanyName", "OBS" - VALUE "FileDescription", "OBS Studio" - VALUE "FileVersion", "${UI_VERSION}" - VALUE "InternalName", "obs" - VALUE "OriginalFilename", "obs" - VALUE "ProductName", "OBS Studio" - VALUE "ProductVersion", "${UI_VERSION}" - VALUE "Comments", "Free and open source software for video recording and live streaming" - VALUE "LegalCopyright", "(C) Lain Bailey" - END - END - - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0409, 0x04B0 - END -END diff --git a/UI/CMakeLists.txt b/frontend/CMakeLists.txt similarity index 55% rename from UI/CMakeLists.txt rename to frontend/CMakeLists.txt index 0917c82c8..380941569 100644 --- a/UI/CMakeLists.txt +++ b/frontend/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.28...3.30) -add_subdirectory(obs-frontend-api) +add_subdirectory(api) -option(ENABLE_UI "Enable building with UI (requires Qt)" ON) +option(ENABLE_FRONTEND "Enable building with UI frontend (requires Qt6)" ON) -if(NOT ENABLE_UI) +if(NOT ENABLE_FRONTEND) target_disable_feature(obs "User Interface") return() else() @@ -15,11 +15,15 @@ find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) find_package(CURL REQUIRED) if(NOT TARGET OBS::json11) - add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" "${CMAKE_BINARY_DIR}/deps/json11") + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" json11) +endif() + +if(NOT TARGET OBS::libobs) + add_subdirectory("${CMAKE_SOURCE_DIR}/libobs" libobs) endif() if(NOT TARGET OBS::bpm) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" "${CMAKE_BINARY_DIR}/shared/bpm") + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" bpm) endif() add_executable(obs-studio) @@ -32,17 +36,23 @@ target_link_libraries( FFmpeg::avcodec FFmpeg::avutil FFmpeg::avformat - OBS::bpm OBS::libobs OBS::frontend-api OBS::json11 + OBS::bpm ) -include(cmake/ui-qt.cmake) -include(cmake/ui-elements.cmake) -include(cmake/ui-windows.cmake) +include(cmake/ui-components.cmake) +include(cmake/ui-dialogs.cmake) +include(cmake/ui-docks.cmake) include(cmake/feature-importers.cmake) +include(cmake/ui-oauth.cmake) include(cmake/feature-browserpanels.cmake) +include(cmake/ui-qt.cmake) +include(cmake/ui-settings.cmake) +include(cmake/ui-utility.cmake) +include(cmake/ui-widgets.cmake) +include(cmake/ui-wizards.cmake) if(NOT OAUTH_BASE_URL) set(OAUTH_BASE_URL "https://auth.obsproject.com/" CACHE STRING "Default OAuth base URL") @@ -53,56 +63,13 @@ include(cmake/feature-restream.cmake) include(cmake/feature-youtube.cmake) include(cmake/feature-whatsnew.cmake) -add_subdirectory(frontend-plugins) +add_subdirectory(plugins) -configure_file(ui-config.h.in ui-config.h) +configure_file(cmake/templates/ui-config.h.in ui-config.h) target_sources( obs-studio - PRIVATE - api-interface.cpp - auth-base.cpp - auth-base.hpp - auth-listener.cpp - auth-listener.hpp - auth-oauth.cpp - auth-oauth.hpp - display-helpers.hpp - ffmpeg-utils.cpp - ffmpeg-utils.hpp - multiview.cpp - multiview.hpp - obf.c - obf.h - obs-app-theming.cpp - obs-app-theming.hpp - obs-app.cpp - obs-app.hpp - obs-proxy-style.cpp - obs-proxy-style.hpp - platform.hpp - qt-display.cpp - qt-display.hpp - ui-config.h - ui-validation.cpp - ui-validation.hpp -) - -target_sources( - obs-studio - PRIVATE - goliveapi-censoredjson.cpp - goliveapi-censoredjson.hpp - goliveapi-network.cpp - goliveapi-network.hpp - goliveapi-postdata.cpp - goliveapi-postdata.hpp - models/multitrack-video.hpp - multitrack-video-error.cpp - multitrack-video-error.hpp - multitrack-video-output.cpp - multitrack-video-output.hpp - system-info.hpp + PRIVATE obs-main.cpp OBSStudioAPI.cpp OBSStudioAPI.hpp OBSApp.cpp OBSApp.hpp OBSApp_Themes.cpp ui-config.h ) if(OS_WINDOWS) @@ -132,4 +99,18 @@ get_property(obs_module_list GLOBAL PROPERTY OBS_MODULES_ENABLED) list(JOIN obs_module_list "|" SAFE_MODULES) target_compile_definitions(obs-studio PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"") +get_target_property(target_sources obs-studio SOURCES) +set(target_cpp_sources ${target_sources}) +set(target_hpp_sources ${target_sources}) +set(target_qt_sources ${target_sources}) +list(FILTER target_cpp_sources INCLUDE REGEX ".+\\.(cpp|mm|c|m)") +list(SORT target_cpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +list(FILTER target_hpp_sources INCLUDE REGEX ".+\\.(hpp|h)") +list(SORT target_hpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +list(FILTER target_qt_sources INCLUDE REGEX ".+\\.(ui|qrc)") +list(SORT target_qt_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${target_cpp_sources}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${target_hpp_sources}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Qt Files" FILES ${target_qt_sources}) + set_target_properties_obs(obs-studio PROPERTIES FOLDER frontend OUTPUT_NAME "$,obs64,obs>") diff --git a/frontend/cmake/feature-browserpanels.cmake b/frontend/cmake/feature-browserpanels.cmake new file mode 100644 index 000000000..04c36695b --- /dev/null +++ b/frontend/cmake/feature-browserpanels.cmake @@ -0,0 +1,18 @@ +if(TARGET OBS::browser-panels) + target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE) + + target_link_libraries(obs-studio PRIVATE OBS::browser-panels) + + target_sources( + obs-studio + PRIVATE + dialogs/OBSExtraBrowsers.cpp + dialogs/OBSExtraBrowsers.hpp + docks/BrowserDock.cpp + docks/BrowserDock.hpp + utility/ExtraBrowsersDelegate.cpp + utility/ExtraBrowsersDelegate.hpp + utility/ExtraBrowsersModel.cpp + utility/ExtraBrowsersModel.hpp + ) +endif() diff --git a/frontend/cmake/feature-importers.cmake b/frontend/cmake/feature-importers.cmake new file mode 100644 index 000000000..b4f54eaca --- /dev/null +++ b/frontend/cmake/feature-importers.cmake @@ -0,0 +1,16 @@ +target_sources( + obs-studio + PRIVATE + importer/ImporterEntryPathItemDelegate.cpp + importer/ImporterEntryPathItemDelegate.hpp + importer/ImporterModel.cpp + importer/ImporterModel.hpp + importer/OBSImporter.cpp + importer/OBSImporter.hpp + importers/classic.cpp + importers/importers.cpp + importers/importers.hpp + importers/sl.cpp + importers/studio.cpp + importers/xsplit.cpp +) diff --git a/UI/cmake/feature-macos-update.cmake b/frontend/cmake/feature-macos-update.cmake similarity index 51% rename from UI/cmake/feature-macos-update.cmake rename to frontend/cmake/feature-macos-update.cmake index 7c6327792..da654babf 100644 --- a/UI/cmake/feature-macos-update.cmake +++ b/frontend/cmake/feature-macos-update.cmake @@ -9,14 +9,16 @@ endif() target_sources( obs-studio PRIVATE - update/crypto-helpers-mac.mm - update/crypto-helpers.hpp - update/models/branches.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.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/UI/cmake/feature-restream.cmake b/frontend/cmake/feature-restream.cmake similarity index 78% rename from UI/cmake/feature-restream.cmake rename to frontend/cmake/feature-restream.cmake index 65847a1ab..5e77f75dd 100644 --- a/UI/cmake/feature-restream.cmake +++ b/frontend/cmake/feature-restream.cmake @@ -1,5 +1,5 @@ if(RESTREAM_CLIENTID AND RESTREAM_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-restream.cpp auth-restream.hpp) + target_sources(obs-studio PRIVATE oauth/RestreamAuth.cpp oauth/RestreamAuth.hpp) target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED) else() target_disable_feature(obs-studio "Restream API connection") diff --git a/UI/cmake/feature-sparkle.cmake b/frontend/cmake/feature-sparkle.cmake similarity index 65% rename from UI/cmake/feature-sparkle.cmake rename to frontend/cmake/feature-sparkle.cmake index eb9fe7d92..3c9935702 100644 --- a/UI/cmake/feature-sparkle.cmake +++ b/frontend/cmake/feature-sparkle.cmake @@ -1,8 +1,17 @@ if(SPARKLE_APPCAST_URL AND SPARKLE_PUBLIC_KEY) find_library(SPARKLE Sparkle) mark_as_advanced(SPARKLE) - target_sources(obs-studio PRIVATE update/mac-update.cpp update/mac-update.hpp update/sparkle-updater.mm) - set_source_files_properties(update/sparkle-updater.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) + target_sources( + obs-studio + PRIVATE + utility/MacUpdateThread.cpp + utility/MacUpdateThread.hpp + utility/OBSSparkle.hpp + utility/OBSSparkle.mm + utility/OBSUpdateDelegate.h + utility/OBSUpdateDelegate.mm + ) + set_source_files_properties(utility/OBSSparkle.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) target_link_libraries(obs-studio PRIVATE "$") diff --git a/UI/cmake/feature-twitch.cmake b/frontend/cmake/feature-twitch.cmake similarity index 78% rename from UI/cmake/feature-twitch.cmake rename to frontend/cmake/feature-twitch.cmake index b8415749f..663c48e05 100644 --- a/UI/cmake/feature-twitch.cmake +++ b/frontend/cmake/feature-twitch.cmake @@ -1,5 +1,5 @@ if(TWITCH_CLIENTID AND TWITCH_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-twitch.cpp auth-twitch.hpp) + target_sources(obs-studio PRIVATE oauth/TwitchAuth.cpp oauth/TwitchAuth.hpp) target_enable_feature(obs-studio "Twitch API connection" TWITCH_ENABLED) else() target_disable_feature(obs-studio "Twitch API connection") diff --git a/UI/cmake/feature-whatsnew.cmake b/frontend/cmake/feature-whatsnew.cmake similarity index 68% rename from UI/cmake/feature-whatsnew.cmake rename to frontend/cmake/feature-whatsnew.cmake index 8e6771821..c7afb49a9 100644 --- a/UI/cmake/feature-whatsnew.cmake +++ b/frontend/cmake/feature-whatsnew.cmake @@ -20,13 +20,15 @@ if(ENABLE_WHATSNEW AND TARGET OBS::browser-panels) target_sources( obs-studio PRIVATE - update/crypto-helpers-mbedtls.cpp - update/crypto-helpers.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.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/UI/cmake/feature-youtube.cmake b/frontend/cmake/feature-youtube.cmake similarity index 55% rename from UI/cmake/feature-youtube.cmake rename to frontend/cmake/feature-youtube.cmake index 4db0e31ad..be87b509f 100644 --- a/UI/cmake/feature-youtube.cmake +++ b/frontend/cmake/feature-youtube.cmake @@ -8,14 +8,17 @@ if( target_sources( obs-studio PRIVATE - auth-youtube.cpp - auth-youtube.hpp - window-dock-youtube-app.cpp - window-dock-youtube-app.hpp - window-youtube-actions.cpp - window-youtube-actions.hpp - youtube-api-wrappers.cpp - youtube-api-wrappers.hpp + dialogs/OBSYoutubeActions.cpp + dialogs/OBSYoutubeActions.hpp + docks/YouTubeAppDock.cpp + docks/YouTubeAppDock.hpp + docks/YouTubeChatDock.cpp + docks/YouTubeChatDock.hpp + forms/OBSYoutubeActions.ui + oauth/YoutubeAuth.cpp + oauth/YoutubeAuth.hpp + utility/YoutubeApiWrappers.cpp + utility/YoutubeApiWrappers.hpp ) target_enable_feature(obs-studio "YouTube API connection" YOUTUBE_ENABLED) diff --git a/UI/cmake/linux/com.obsproject.Studio.desktop b/frontend/cmake/linux/com.obsproject.Studio.desktop similarity index 100% rename from UI/cmake/linux/com.obsproject.Studio.desktop rename to frontend/cmake/linux/com.obsproject.Studio.desktop diff --git a/UI/cmake/linux/com.obsproject.Studio.metainfo.xml.in b/frontend/cmake/linux/com.obsproject.Studio.metainfo.xml.in similarity index 100% rename from UI/cmake/linux/com.obsproject.Studio.metainfo.xml.in rename to frontend/cmake/linux/com.obsproject.Studio.metainfo.xml.in diff --git a/UI/cmake/linux/icons/obs-logo-128.png b/frontend/cmake/linux/icons/obs-logo-128.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-128.png rename to frontend/cmake/linux/icons/obs-logo-128.png diff --git a/UI/cmake/linux/icons/obs-logo-256.png b/frontend/cmake/linux/icons/obs-logo-256.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-256.png rename to frontend/cmake/linux/icons/obs-logo-256.png diff --git a/UI/cmake/linux/icons/obs-logo-512.png b/frontend/cmake/linux/icons/obs-logo-512.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-512.png rename to frontend/cmake/linux/icons/obs-logo-512.png diff --git a/UI/cmake/linux/icons/obs-logo-scalable.svg b/frontend/cmake/linux/icons/obs-logo-scalable.svg similarity index 100% rename from UI/cmake/linux/icons/obs-logo-scalable.svg rename to frontend/cmake/linux/icons/obs-logo-scalable.svg diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/Contents.json b/frontend/cmake/macos/Assets.xcassets/Contents.json similarity index 100% rename from UI/cmake/macos/Assets.xcassets/Contents.json rename to frontend/cmake/macos/Assets.xcassets/Contents.json diff --git a/UI/cmake/macos/Info.plist.in b/frontend/cmake/macos/Info.plist.in similarity index 100% rename from UI/cmake/macos/Info.plist.in rename to frontend/cmake/macos/Info.plist.in diff --git a/UI/cmake/macos/entitlements-extension.plist b/frontend/cmake/macos/entitlements-extension.plist similarity index 100% rename from UI/cmake/macos/entitlements-extension.plist rename to frontend/cmake/macos/entitlements-extension.plist diff --git a/UI/cmake/macos/entitlements.plist b/frontend/cmake/macos/entitlements.plist similarity index 100% rename from UI/cmake/macos/entitlements.plist rename to frontend/cmake/macos/entitlements.plist diff --git a/UI/cmake/macos/exportOptions-extension.plist.in b/frontend/cmake/macos/exportOptions-extension.plist.in similarity index 100% rename from UI/cmake/macos/exportOptions-extension.plist.in rename to frontend/cmake/macos/exportOptions-extension.plist.in diff --git a/UI/cmake/macos/exportOptions.plist.in b/frontend/cmake/macos/exportOptions.plist.in similarity index 100% rename from UI/cmake/macos/exportOptions.plist.in rename to frontend/cmake/macos/exportOptions.plist.in diff --git a/UI/cmake/macos/qt.conf b/frontend/cmake/macos/qt.conf similarity index 100% rename from UI/cmake/macos/qt.conf rename to frontend/cmake/macos/qt.conf diff --git a/UI/cmake/os-freebsd.cmake b/frontend/cmake/os-freebsd.cmake similarity index 92% rename from UI/cmake/os-freebsd.cmake rename to frontend/cmake/os-freebsd.cmake index 93544ad16..d06d73874 100644 --- a/UI/cmake/os-freebsd.cmake +++ b/frontend/cmake/os-freebsd.cmake @@ -1,9 +1,7 @@ -target_sources(obs-studio PRIVATE platform-x11.cpp) +target_sources(obs-studio PRIVATE 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) -target_sources(obs-studio PRIVATE system-info-posix.cpp) - if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-linux.cmake b/frontend/cmake/os-linux.cmake similarity index 95% rename from UI/cmake/os-linux.cmake rename to frontend/cmake/os-linux.cmake index ce9f873d7..a32a3312e 100644 --- a/UI/cmake/os-linux.cmake +++ b/frontend/cmake/os-linux.cmake @@ -1,12 +1,10 @@ -target_sources(obs-studio PRIVATE platform-x11.cpp) +target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp) target_compile_definitions( obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" $<$:ENABLE_PORTABLE_CONFIG> ) target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus) -target_sources(obs-studio PRIVATE system-info-posix.cpp) - if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-macos.cmake b/frontend/cmake/os-macos.cmake similarity index 75% rename from UI/cmake/os-macos.cmake rename to frontend/cmake/os-macos.cmake index a965f1bff..0bd51c923 100644 --- a/UI/cmake/os-macos.cmake +++ b/frontend/cmake/os-macos.cmake @@ -1,10 +1,16 @@ include(cmake/feature-sparkle.cmake) -target_sources(obs-studio PRIVATE platform-osx.mm forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) +target_sources( + obs-studio + PRIVATE + dialogs/OBSPermissions.cpp + dialogs/OBSPermissions.hpp + forms/OBSPermissions.ui + utility/platform-osx.mm + utility/system-info-macos.mm +) target_compile_options(obs-studio PRIVATE -Wno-quoted-include-in-framework-header -Wno-comma) -target_sources(obs-studio PRIVATE system-info-macos.mm) - set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 14.0.3) diff --git a/UI/cmake/os-windows.cmake b/frontend/cmake/os-windows.cmake similarity index 57% rename from UI/cmake/os-windows.cmake rename to frontend/cmake/os-windows.cmake index f781c9db5..9c77a0263 100644 --- a/UI/cmake/os-windows.cmake +++ b/frontend/cmake/os-windows.cmake @@ -18,42 +18,48 @@ target_sources( obs-studio PRIVATE cmake/windows/obs.manifest + dialogs/OBSUpdate.cpp + dialogs/OBSUpdate.hpp + forms/OBSUpdate.ui obs.rc - platform-windows.cpp - update/crypto-helpers-mbedtls.cpp - update/crypto-helpers.hpp - update/models/branches.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.hpp - update/update-window.cpp - update/update-window.hpp - update/win-update.cpp - update/win-update.hpp - win-dll-blocklist.c - win-update/updater/manifest.hpp + utility/AutoUpdateThread.cpp + utility/AutoUpdateThread.hpp + utility/crypto-helpers-mbedtls.cpp + utility/crypto-helpers.hpp + utility/models/branches.hpp + utility/models/whatsnew.hpp + utility/platform-windows.cpp + 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 ) -target_sources(obs-studio PRIVATE system-info-windows.cpp) +add_library(obs-updater-manifest INTERFACE) +add_library(OBS::updater-manifest ALIAS obs-updater-manifest) + +target_sources(obs-updater-manifest INTERFACE updater/manifest.hpp) target_link_libraries( obs-studio - PRIVATE crypt32 OBS::blake2 OBS::w32-pthreads MbedTLS::mbedtls nlohmann_json::nlohmann_json Detours::Detours + PRIVATE + crypt32 + OBS::blake2 + OBS::updater-manifest + OBS::w32-pthreads + MbedTLS::mbedtls + nlohmann_json::nlohmann_json + Detours::Detours ) target_compile_definitions(obs-studio PRIVATE PSAPI_VERSION=2) target_link_options(obs-studio PRIVATE /IGNORE:4099 $<$:/NODEFAULTLIB:MSVCRT>) -add_library(obs-update-helpers INTERFACE) -add_library(OBS::update-helpers ALIAS obs-update-helpers) - -target_sources(obs-update-helpers INTERFACE win-update/win-update-helpers.cpp win-update/win-update-helpers.hpp) - -target_include_directories(obs-update-helpers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/win-update") - # Set commit for untagged version comparisons in the Windows updater if(OBS_VERSION MATCHES ".+g[a-f0-9]+.*") string(REGEX REPLACE ".+g([a-f0-9]+).*$" "\\1" OBS_COMMIT ${OBS_VERSION}) @@ -61,9 +67,9 @@ else() set(OBS_COMMIT "") endif() -set_source_files_properties(update/win-update.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}") +set_source_files_properties(utility/AutoUpdateThread.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}") -add_subdirectory(win-update/updater) +add_subdirectory(updater) set_property(TARGET obs-studio APPEND PROPERTY AUTORCC_OPTIONS --format-version 1) diff --git a/UI/ui-config.h.in b/frontend/cmake/templates/ui-config.h.in similarity index 100% rename from UI/ui-config.h.in rename to frontend/cmake/templates/ui-config.h.in diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake new file mode 100644 index 000000000..acea2d66c --- /dev/null +++ b/frontend/cmake/ui-components.cmake @@ -0,0 +1,85 @@ +if(NOT TARGET OBS::qt-slider-ignorewheel) + add_subdirectory( + "${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel" + "${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel" + ) +endif() + +target_link_libraries(obs-studio PRIVATE OBS::qt-slider-ignorewheel) + +target_sources( + obs-studio + PRIVATE + components/AbsoluteSlider.cpp + components/AbsoluteSlider.hpp + components/ApplicationAudioCaptureToolbar.cpp + components/ApplicationAudioCaptureToolbar.hpp + components/AudioCaptureToolbar.cpp + components/AudioCaptureToolbar.hpp + components/BalanceSlider.hpp + components/BrowserToolbar.cpp + components/BrowserToolbar.hpp + components/ClickableLabel.hpp + components/ColorSourceToolbar.cpp + components/ColorSourceToolbar.hpp + components/ComboSelectToolbar.cpp + components/ComboSelectToolbar.hpp + components/DelButton.hpp + components/DeviceCaptureToolbar.cpp + components/DeviceCaptureToolbar.hpp + components/DisplayCaptureToolbar.cpp + components/DisplayCaptureToolbar.hpp + components/EditWidget.hpp + components/FocusList.cpp + components/FocusList.hpp + components/GameCaptureToolbar.cpp + components/GameCaptureToolbar.hpp + components/HScrollArea.cpp + components/HScrollArea.hpp + components/ImageSourceToolbar.cpp + components/ImageSourceToolbar.hpp + components/MediaControls.cpp + components/MediaControls.hpp + components/MenuButton.cpp + components/MenuButton.hpp + components/Multiview.cpp + components/Multiview.hpp + components/MuteCheckBox.hpp + components/NonCheckableButton.hpp + components/OBSAdvAudioCtrl.cpp + components/OBSAdvAudioCtrl.hpp + components/OBSPreviewScalingComboBox.cpp + components/OBSPreviewScalingComboBox.hpp + components/OBSPreviewScalingLabel.cpp + components/OBSPreviewScalingLabel.hpp + components/OBSSourceLabel.cpp + components/OBSSourceLabel.hpp + components/SceneTree.cpp + components/SceneTree.hpp + components/SilentUpdateCheckBox.hpp + components/SilentUpdateSpinBox.hpp + components/SourceToolbar.cpp + components/SourceToolbar.hpp + components/SourceTree.cpp + components/SourceTree.hpp + components/SourceTreeDelegate.cpp + components/SourceTreeDelegate.hpp + components/SourceTreeItem.cpp + components/SourceTreeItem.hpp + components/SourceTreeModel.cpp + components/SourceTreeModel.hpp + components/TextSourceToolbar.cpp + components/TextSourceToolbar.hpp + components/UIValidation.cpp + components/UIValidation.hpp + components/UrlPushButton.cpp + components/UrlPushButton.hpp + components/VisibilityItemDelegate.cpp + components/VisibilityItemDelegate.hpp + components/VisibilityItemWidget.cpp + components/VisibilityItemWidget.hpp + components/VolumeSlider.cpp + components/VolumeSlider.hpp + components/WindowCaptureToolbar.cpp + components/WindowCaptureToolbar.hpp +) diff --git a/frontend/cmake/ui-dialogs.cmake b/frontend/cmake/ui-dialogs.cmake new file mode 100644 index 000000000..dfedb2b21 --- /dev/null +++ b/frontend/cmake/ui-dialogs.cmake @@ -0,0 +1,40 @@ +if(NOT TARGET OBS::properties-view) + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view") +endif() + +target_link_libraries(obs-studio PRIVATE OBS::properties-view) + +target_sources( + obs-studio + PRIVATE + dialogs/NameDialog.cpp + dialogs/NameDialog.hpp + dialogs/OAuthLogin.cpp + dialogs/OAuthLogin.hpp + dialogs/OBSAbout.cpp + dialogs/OBSAbout.hpp + dialogs/OBSBasicAdvAudio.cpp + dialogs/OBSBasicAdvAudio.hpp + dialogs/OBSBasicFilters.cpp + dialogs/OBSBasicFilters.hpp + dialogs/OBSBasicInteraction.cpp + dialogs/OBSBasicInteraction.hpp + dialogs/OBSBasicProperties.cpp + dialogs/OBSBasicProperties.hpp + dialogs/OBSBasicSourceSelect.cpp + dialogs/OBSBasicSourceSelect.hpp + dialogs/OBSBasicTransform.cpp + dialogs/OBSBasicTransform.hpp + dialogs/OBSBasicVCamConfig.cpp + dialogs/OBSBasicVCamConfig.hpp + dialogs/OBSLogReply.cpp + dialogs/OBSLogReply.hpp + dialogs/OBSLogViewer.cpp + dialogs/OBSLogViewer.hpp + dialogs/OBSMissingFiles.cpp + dialogs/OBSMissingFiles.hpp + dialogs/OBSRemux.cpp + dialogs/OBSRemux.hpp + dialogs/OBSWhatsNew.cpp + dialogs/OBSWhatsNew.hpp +) diff --git a/frontend/cmake/ui-docks.cmake b/frontend/cmake/ui-docks.cmake new file mode 100644 index 000000000..0de10de2a --- /dev/null +++ b/frontend/cmake/ui-docks.cmake @@ -0,0 +1 @@ +target_sources(obs-studio PRIVATE docks/OBSDock.cpp docks/OBSDock.hpp) diff --git a/frontend/cmake/ui-oauth.cmake b/frontend/cmake/ui-oauth.cmake new file mode 100644 index 000000000..b48f845e8 --- /dev/null +++ b/frontend/cmake/ui-oauth.cmake @@ -0,0 +1,4 @@ +target_sources( + obs-studio + PRIVATE oauth/Auth.cpp oauth/Auth.hpp oauth/AuthListener.cpp oauth/AuthListener.hpp oauth/OAuth.cpp oauth/OAuth.hpp +) diff --git a/frontend/cmake/ui-qt.cmake b/frontend/cmake/ui-qt.cmake new file mode 100644 index 000000000..c4293e0eb --- /dev/null +++ b/frontend/cmake/ui-qt.cmake @@ -0,0 +1,58 @@ +find_package(Qt6 REQUIRED Widgets Network Svg Xml) + +if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) + find_package(Qt6 REQUIRED Gui DBus) +endif() + +if(NOT TARGET OBS::qt-wrappers) + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers") +endif() + +target_link_libraries( + obs-studio + PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers +) + +set_target_properties( + obs-studio + PROPERTIES AUTOMOC TRUE AUTOUIC TRUE AUTORCC TRUE AUTOGEN_PARALLEL AUTO +) + +set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar) + +target_sources( + obs-studio + PRIVATE + forms/AutoConfigFinishPage.ui + forms/AutoConfigStartPage.ui + forms/AutoConfigStartPage.ui + forms/AutoConfigStreamPage.ui + forms/AutoConfigTestPage.ui + forms/AutoConfigVideoPage.ui + forms/ColorSelect.ui + forms/obs.qrc + forms/OBSAbout.ui + forms/OBSAdvAudio.ui + forms/OBSBasic.ui + forms/OBSBasicControls.ui + forms/OBSBasicFilters.ui + forms/OBSBasicInteraction.ui + forms/OBSBasicProperties.ui + forms/OBSBasicSettings.ui + forms/OBSBasicSourceSelect.ui + forms/OBSBasicVCamConfig.ui + forms/OBSExtraBrowsers.ui + forms/OBSImporter.ui + forms/OBSLogReply.ui + forms/OBSLogReply.ui + forms/OBSMissingFiles.ui + forms/OBSRemux.ui + forms/source-toolbar/browser-source-toolbar.ui + forms/source-toolbar/color-source-toolbar.ui + forms/source-toolbar/device-select-toolbar.ui + forms/source-toolbar/game-capture-toolbar.ui + forms/source-toolbar/image-source-toolbar.ui + forms/source-toolbar/media-controls.ui + forms/source-toolbar/text-source-toolbar.ui + forms/StatusBarWidget.ui +) diff --git a/frontend/cmake/ui-settings.cmake b/frontend/cmake/ui-settings.cmake new file mode 100644 index 000000000..cea9e5d52 --- /dev/null +++ b/frontend/cmake/ui-settings.cmake @@ -0,0 +1,15 @@ +target_sources( + obs-studio + PRIVATE + settings/OBSBasicSettings_A11y.cpp + settings/OBSBasicSettings_Appearance.cpp + settings/OBSBasicSettings_Stream.cpp + settings/OBSBasicSettings.cpp + settings/OBSBasicSettings.hpp + settings/OBSHotkeyEdit.cpp + settings/OBSHotkeyEdit.hpp + settings/OBSHotkeyLabel.cpp + settings/OBSHotkeyLabel.hpp + settings/OBSHotkeyWidget.cpp + settings/OBSHotkeyWidget.hpp +) diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake new file mode 100644 index 000000000..88535faff --- /dev/null +++ b/frontend/cmake/ui-utility.cmake @@ -0,0 +1,69 @@ +target_sources( + obs-studio + 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/display-helpers.hpp + utility/FFmpegCodec.cpp + utility/FFmpegCodec.hpp + utility/FFmpegFormat.cpp + utility/FFmpegFormat.hpp + utility/FFmpegShared.hpp + utility/GoLiveAPI_CensoredJson.cpp + utility/GoLiveAPI_CensoredJson.hpp + utility/GoLiveAPI_Network.cpp + 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/OBSEventFilter.hpp + utility/OBSProxyStyle.cpp + utility/OBSProxyStyle.hpp + utility/OBSTheme.hpp + utility/OBSThemeVariable.hpp + utility/OBSTranslator.cpp + utility/OBSTranslator.hpp + utility/platform.hpp + utility/QuickTransition.cpp + utility/QuickTransition.hpp + utility/RemoteTextThread.cpp + utility/RemoteTextThread.hpp + utility/RemuxEntryPathItemDelegate.cpp + utility/RemuxEntryPathItemDelegate.hpp + utility/RemuxQueueModel.cpp + utility/RemuxQueueModel.hpp + utility/RemuxWorker.cpp + utility/RemuxWorker.hpp + utility/SceneRenameDelegate.cpp + utility/SceneRenameDelegate.hpp + utility/ScreenshotObj.cpp + utility/ScreenshotObj.hpp + utility/SettingsEventFilter.hpp + utility/SimpleOutput.cpp + 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 +) diff --git a/frontend/cmake/ui-widgets.cmake b/frontend/cmake/ui-widgets.cmake new file mode 100644 index 000000000..20befdeb0 --- /dev/null +++ b/frontend/cmake/ui-widgets.cmake @@ -0,0 +1,66 @@ +if(NOT TARGET OBS::qt-vertical-scroll-area) + add_subdirectory( + "${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area" + "${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area" + ) +endif() + +target_link_libraries(obs-studio PRIVATE OBS::qt-vertical-scroll-area) + +target_sources( + obs-studio + PRIVATE + widgets/ColorSelect.cpp + widgets/ColorSelect.hpp + widgets/OBSBasic.cpp + widgets/OBSBasic.hpp + widgets/OBSBasic_Browser.cpp + widgets/OBSBasic_Clipboard.cpp + widgets/OBSBasic_ContextToolbar.cpp + widgets/OBSBasic_Docks.cpp + widgets/OBSBasic_Dropfiles.cpp + widgets/OBSBasic_Hotkeys.cpp + widgets/OBSBasic_Icons.cpp + widgets/OBSBasic_MainControls.cpp + widgets/OBSBasic_OutputHandler.cpp + widgets/OBSBasic_Preview.cpp + widgets/OBSBasic_Profiles.cpp + widgets/OBSBasic_Projectors.cpp + widgets/OBSBasic_Recording.cpp + widgets/OBSBasic_ReplayBuffer.cpp + widgets/OBSBasic_SceneCollections.cpp + widgets/OBSBasic_SceneItems.cpp + widgets/OBSBasic_Scenes.cpp + widgets/OBSBasic_Screenshots.cpp + widgets/OBSBasic_Service.cpp + widgets/OBSBasic_StatusBar.cpp + widgets/OBSBasic_Streaming.cpp + widgets/OBSBasic_StudioMode.cpp + widgets/OBSBasic_SysTray.cpp + widgets/OBSBasic_Transitions.cpp + widgets/OBSBasic_Updater.cpp + widgets/OBSBasic_VirtualCam.cpp + widgets/OBSBasic_VolControl.cpp + widgets/OBSBasic_YouTube.cpp + widgets/OBSBasicControls.cpp + widgets/OBSBasicControls.hpp + widgets/OBSBasicPreview.cpp + widgets/OBSBasicPreview.hpp + widgets/OBSBasicStats.cpp + widgets/OBSBasicStats.hpp + widgets/OBSBasicStatusBar.cpp + widgets/OBSBasicStatusBar.hpp + widgets/OBSMainWindow.hpp + widgets/OBSProjector.cpp + widgets/OBSProjector.hpp + widgets/OBSQTDisplay.cpp + widgets/OBSQTDisplay.hpp + widgets/StatusBarWidget.cpp + widgets/StatusBarWidget.hpp + widgets/VolControl.cpp + widgets/VolControl.hpp + widgets/VolumeAccessibleInterface.cpp + widgets/VolumeAccessibleInterface.hpp + widgets/VolumeMeter.cpp + widgets/VolumeMeter.hpp +) diff --git a/frontend/cmake/ui-wizards.cmake b/frontend/cmake/ui-wizards.cmake new file mode 100644 index 000000000..2dfc3f647 --- /dev/null +++ b/frontend/cmake/ui-wizards.cmake @@ -0,0 +1,15 @@ +target_sources( + obs-studio + PRIVATE + wizards/AutoConfig.cpp + wizards/AutoConfig.hpp + wizards/AutoConfigStartPage.cpp + wizards/AutoConfigStartPage.hpp + wizards/AutoConfigStreamPage.cpp + wizards/AutoConfigStreamPage.hpp + wizards/AutoConfigTestPage.cpp + wizards/AutoConfigTestPage.hpp + wizards/AutoConfigVideoPage.cpp + wizards/AutoConfigVideoPage.hpp + wizards/TestMode.hpp +) diff --git a/UI/cmake/windows/obs-studio.ico b/frontend/cmake/windows/obs-studio.ico similarity index 100% rename from UI/cmake/windows/obs-studio.ico rename to frontend/cmake/windows/obs-studio.ico diff --git a/UI/cmake/windows/obs.manifest b/frontend/cmake/windows/obs.manifest similarity index 100% rename from UI/cmake/windows/obs.manifest rename to frontend/cmake/windows/obs.manifest diff --git a/UI/cmake/windows/obs.rc.in b/frontend/cmake/windows/obs.rc.in similarity index 100% rename from UI/cmake/windows/obs.rc.in rename to frontend/cmake/windows/obs.rc.in From 076efedc827b48ad86fe69aca1f870010d1f913c Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:14:48 +0100 Subject: [PATCH 30/37] frontend: Migrate obs-frontend-api --- {UI/obs-frontend-api => frontend/api}/CMakeLists.txt | 0 .../api}/cmake/linux/obs-frontend-api.pc.in | 0 .../api}/cmake/obs-frontend-apiConfig.cmake.in | 0 .../api}/cmake/windows/obs-module.rc.in | 0 {UI/obs-frontend-api => frontend/api}/obs-frontend-api.cpp | 0 {UI/obs-frontend-api => frontend/api}/obs-frontend-api.h | 0 {UI/obs-frontend-api => frontend/api}/obs-frontend-internal.hpp | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {UI/obs-frontend-api => frontend/api}/CMakeLists.txt (100%) rename {UI/obs-frontend-api => frontend/api}/cmake/linux/obs-frontend-api.pc.in (100%) rename {UI/obs-frontend-api => frontend/api}/cmake/obs-frontend-apiConfig.cmake.in (100%) rename {UI/obs-frontend-api => frontend/api}/cmake/windows/obs-module.rc.in (100%) rename {UI/obs-frontend-api => frontend/api}/obs-frontend-api.cpp (100%) rename {UI/obs-frontend-api => frontend/api}/obs-frontend-api.h (100%) rename {UI/obs-frontend-api => frontend/api}/obs-frontend-internal.hpp (100%) diff --git a/UI/obs-frontend-api/CMakeLists.txt b/frontend/api/CMakeLists.txt similarity index 100% rename from UI/obs-frontend-api/CMakeLists.txt rename to frontend/api/CMakeLists.txt diff --git a/UI/obs-frontend-api/cmake/linux/obs-frontend-api.pc.in b/frontend/api/cmake/linux/obs-frontend-api.pc.in similarity index 100% rename from UI/obs-frontend-api/cmake/linux/obs-frontend-api.pc.in rename to frontend/api/cmake/linux/obs-frontend-api.pc.in diff --git a/UI/obs-frontend-api/cmake/obs-frontend-apiConfig.cmake.in b/frontend/api/cmake/obs-frontend-apiConfig.cmake.in similarity index 100% rename from UI/obs-frontend-api/cmake/obs-frontend-apiConfig.cmake.in rename to frontend/api/cmake/obs-frontend-apiConfig.cmake.in diff --git a/UI/obs-frontend-api/cmake/windows/obs-module.rc.in b/frontend/api/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/obs-frontend-api/cmake/windows/obs-module.rc.in rename to frontend/api/cmake/windows/obs-module.rc.in diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/frontend/api/obs-frontend-api.cpp similarity index 100% rename from UI/obs-frontend-api/obs-frontend-api.cpp rename to frontend/api/obs-frontend-api.cpp diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/frontend/api/obs-frontend-api.h similarity index 100% rename from UI/obs-frontend-api/obs-frontend-api.h rename to frontend/api/obs-frontend-api.h diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/frontend/api/obs-frontend-internal.hpp similarity index 100% rename from UI/obs-frontend-api/obs-frontend-internal.hpp rename to frontend/api/obs-frontend-internal.hpp From 5abfc7c565eb4b681e67ea0adf452aec8195000f Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:17:56 +0100 Subject: [PATCH 31/37] frontend: Migrate OBS Studio data directory --- {UI => frontend}/data/OBSPublicRSAKey.pem | 0 {UI => frontend}/data/images/overflow.png | Bin {UI => frontend}/data/license/gplv2.txt | 0 {UI => frontend}/data/locale.ini | 0 {UI => frontend}/data/locale/af-ZA.ini | 0 {UI => frontend}/data/locale/an-ES.ini | 0 {UI => frontend}/data/locale/ar-SA.ini | 0 {UI => frontend}/data/locale/az-AZ.ini | 0 {UI => frontend}/data/locale/ba-RU.ini | 0 {UI => frontend}/data/locale/be-BY.ini | 0 {UI => frontend}/data/locale/bem-ZM.ini | 0 {UI => frontend}/data/locale/bg-BG.ini | 0 {UI => frontend}/data/locale/bn-BD.ini | 0 {UI => frontend}/data/locale/ca-ES.ini | 0 {UI => frontend}/data/locale/cs-CZ.ini | 0 {UI => frontend}/data/locale/da-DK.ini | 0 {UI => frontend}/data/locale/de-DE.ini | 0 {UI => frontend}/data/locale/el-GR.ini | 0 {UI => frontend}/data/locale/en-GB.ini | 0 {UI => frontend}/data/locale/en-US.ini | 0 {UI => frontend}/data/locale/eo-UY.ini | 0 {UI => frontend}/data/locale/es-ES.ini | 0 {UI => frontend}/data/locale/et-EE.ini | 0 {UI => frontend}/data/locale/eu-ES.ini | 0 {UI => frontend}/data/locale/fa-IR.ini | 0 {UI => frontend}/data/locale/fi-FI.ini | 0 {UI => frontend}/data/locale/fil-PH.ini | 0 {UI => frontend}/data/locale/fr-FR.ini | 0 {UI => frontend}/data/locale/gd-GB.ini | 0 {UI => frontend}/data/locale/gl-ES.ini | 0 {UI => frontend}/data/locale/he-IL.ini | 0 {UI => frontend}/data/locale/hi-IN.ini | 0 {UI => frontend}/data/locale/hr-HR.ini | 0 {UI => frontend}/data/locale/hu-HU.ini | 0 {UI => frontend}/data/locale/hy-AM.ini | 0 {UI => frontend}/data/locale/id-ID.ini | 0 {UI => frontend}/data/locale/is-IS.ini | 0 {UI => frontend}/data/locale/it-IT.ini | 0 {UI => frontend}/data/locale/ja-JP.ini | 0 {UI => frontend}/data/locale/ka-GE.ini | 0 {UI => frontend}/data/locale/kaa.ini | 0 {UI => frontend}/data/locale/kab-KAB.ini | 0 {UI => frontend}/data/locale/kmr-TR.ini | 0 {UI => frontend}/data/locale/ko-KR.ini | 0 {UI => frontend}/data/locale/lo-LA.ini | 0 {UI => frontend}/data/locale/lt-LT.ini | 0 {UI => frontend}/data/locale/lv-LV.ini | 0 {UI => frontend}/data/locale/mn-MN.ini | 0 {UI => frontend}/data/locale/ms-MY.ini | 0 {UI => frontend}/data/locale/nb-NO.ini | 0 {UI => frontend}/data/locale/nl-NL.ini | 0 {UI => frontend}/data/locale/nn-NO.ini | 0 {UI => frontend}/data/locale/oc-FR.ini | 0 {UI => frontend}/data/locale/pa-IN.ini | 0 {UI => frontend}/data/locale/pl-PL.ini | 0 {UI => frontend}/data/locale/pt-BR.ini | 0 {UI => frontend}/data/locale/pt-PT.ini | 0 {UI => frontend}/data/locale/ro-RO.ini | 0 {UI => frontend}/data/locale/ru-RU.ini | 0 {UI => frontend}/data/locale/si-LK.ini | 0 {UI => frontend}/data/locale/sk-SK.ini | 0 {UI => frontend}/data/locale/sl-SI.ini | 0 {UI => frontend}/data/locale/sq-AL.ini | 0 {UI => frontend}/data/locale/sr-CS.ini | 0 {UI => frontend}/data/locale/sr-SP.ini | 0 {UI => frontend}/data/locale/sv-SE.ini | 0 {UI => frontend}/data/locale/szl-PL.ini | 0 {UI => frontend}/data/locale/ta-IN.ini | 0 {UI => frontend}/data/locale/te-IN.ini | 0 {UI => frontend}/data/locale/th-TH.ini | 0 {UI => frontend}/data/locale/tl-PH.ini | 0 {UI => frontend}/data/locale/tr-TR.ini | 0 {UI => frontend}/data/locale/tt-RU.ini | 0 {UI => frontend}/data/locale/ug-CN.ini | 0 {UI => frontend}/data/locale/uk-UA.ini | 0 {UI => frontend}/data/locale/ur-PK.ini | 0 {UI => frontend}/data/locale/vi-VN.ini | 0 {UI => frontend}/data/locale/zh-CN.ini | 0 {UI => frontend}/data/locale/zh-TW.ini | 0 {UI => frontend}/data/themes/Acri/bot_hook.png | Bin {UI => frontend}/data/themes/Acri/bot_hook2.png | Bin .../data/themes/Acri/checkbox_checked.png | Bin .../data/themes/Acri/checkbox_checked_disabled.png | Bin .../data/themes/Acri/checkbox_checked_focus.png | Bin .../data/themes/Acri/checkbox_unchecked.png | Bin .../themes/Acri/checkbox_unchecked_disabled.png | Bin .../data/themes/Acri/checkbox_unchecked_focus.png | Bin {UI => frontend}/data/themes/Acri/radio_checked.png | Bin .../data/themes/Acri/radio_checked_disabled.png | Bin .../data/themes/Acri/radio_checked_focus.png | Bin .../data/themes/Acri/radio_unchecked.png | Bin .../data/themes/Acri/radio_unchecked_disabled.png | Bin .../data/themes/Acri/radio_unchecked_focus.png | Bin {UI => frontend}/data/themes/Acri/sizegrip.png | Bin {UI => frontend}/data/themes/Acri/top_hook.png | Bin {UI => frontend}/data/themes/Dark/alert.svg | 0 {UI => frontend}/data/themes/Dark/close.svg | 0 {UI => frontend}/data/themes/Dark/cogs.svg | 0 {UI => frontend}/data/themes/Dark/collapse.svg | 0 {UI => frontend}/data/themes/Dark/dots-vert.svg | 0 {UI => frontend}/data/themes/Dark/dots.svg | 0 {UI => frontend}/data/themes/Dark/down.svg | 0 {UI => frontend}/data/themes/Dark/entry-clear.svg | 0 {UI => frontend}/data/themes/Dark/expand.svg | 0 {UI => frontend}/data/themes/Dark/filter.svg | 0 {UI => frontend}/data/themes/Dark/interact.svg | 0 {UI => frontend}/data/themes/Dark/left.svg | 0 {UI => frontend}/data/themes/Dark/locked.svg | 0 {UI => frontend}/data/themes/Dark/media-pause.svg | 0 .../data/themes/Dark/media/media_next.svg | 0 .../data/themes/Dark/media/media_pause.svg | 0 .../data/themes/Dark/media/media_play.svg | 0 .../data/themes/Dark/media/media_previous.svg | 0 .../data/themes/Dark/media/media_restart.svg | 0 .../data/themes/Dark/media/media_stop.svg | 0 {UI => frontend}/data/themes/Dark/minus.svg | 0 {UI => frontend}/data/themes/Dark/mute.svg | 0 .../data/themes/Dark/network-disconnected.svg | 0 .../data/themes/Dark/network-inactive.svg | 0 {UI => frontend}/data/themes/Dark/no_sources.svg | 0 {UI => frontend}/data/themes/Dark/plus.svg | 0 {UI => frontend}/data/themes/Dark/popout.svg | 0 .../data/themes/Dark/recording-inactive.svg | 0 .../data/themes/Dark/recording-pause-inactive.svg | 0 {UI => frontend}/data/themes/Dark/refresh.svg | 0 {UI => frontend}/data/themes/Dark/revert.svg | 0 {UI => frontend}/data/themes/Dark/right.svg | 0 {UI => frontend}/data/themes/Dark/save.svg | 0 .../data/themes/Dark/settings/accessibility.svg | 0 .../data/themes/Dark/settings/advanced.svg | 0 .../data/themes/Dark/settings/appearance.svg | 0 .../data/themes/Dark/settings/audio.svg | 0 .../data/themes/Dark/settings/general.svg | 0 .../data/themes/Dark/settings/hotkeys.svg | 0 .../data/themes/Dark/settings/output.svg | 0 .../data/themes/Dark/settings/stream.svg | 0 .../data/themes/Dark/settings/video.svg | 0 {UI => frontend}/data/themes/Dark/sources/brush.svg | 0 .../data/themes/Dark/sources/camera.svg | 0 .../data/themes/Dark/sources/default.svg | 0 .../data/themes/Dark/sources/gamepad.svg | 0 {UI => frontend}/data/themes/Dark/sources/globe.svg | 0 {UI => frontend}/data/themes/Dark/sources/group.svg | 0 {UI => frontend}/data/themes/Dark/sources/image.svg | 0 {UI => frontend}/data/themes/Dark/sources/media.svg | 0 .../data/themes/Dark/sources/microphone.svg | 0 {UI => frontend}/data/themes/Dark/sources/scene.svg | 0 .../data/themes/Dark/sources/slideshow.svg | 0 {UI => frontend}/data/themes/Dark/sources/text.svg | 0 .../data/themes/Dark/sources/window.svg | 0 .../data/themes/Dark/sources/windowaudio.svg | 0 .../data/themes/Dark/streaming-inactive.svg | 0 {UI => frontend}/data/themes/Dark/trash.svg | 0 {UI => frontend}/data/themes/Dark/unassigned.svg | 0 {UI => frontend}/data/themes/Dark/up.svg | 0 {UI => frontend}/data/themes/Dark/updown.svg | 0 {UI => frontend}/data/themes/Dark/visible.svg | 0 {UI => frontend}/data/themes/Light/alert.svg | 0 .../data/themes/Light/checkbox_checked.svg | 0 .../data/themes/Light/checkbox_checked_disabled.svg | 0 .../data/themes/Light/checkbox_checked_focus.svg | 0 .../data/themes/Light/checkbox_unchecked.svg | 0 .../themes/Light/checkbox_unchecked_disabled.svg | 0 .../data/themes/Light/checkbox_unchecked_focus.svg | 0 {UI => frontend}/data/themes/Light/close.svg | 0 {UI => frontend}/data/themes/Light/cogs.svg | 0 {UI => frontend}/data/themes/Light/collapse.svg | 0 {UI => frontend}/data/themes/Light/dots-vert.svg | 0 {UI => frontend}/data/themes/Light/dots.svg | 0 {UI => frontend}/data/themes/Light/down.svg | 0 {UI => frontend}/data/themes/Light/entry-clear.svg | 0 {UI => frontend}/data/themes/Light/expand.svg | 0 {UI => frontend}/data/themes/Light/filter.svg | 0 {UI => frontend}/data/themes/Light/interact.svg | 0 {UI => frontend}/data/themes/Light/left.svg | 0 {UI => frontend}/data/themes/Light/locked.svg | 0 {UI => frontend}/data/themes/Light/media-pause.svg | 0 .../data/themes/Light/media/media_next.svg | 0 .../data/themes/Light/media/media_pause.svg | 0 .../data/themes/Light/media/media_play.svg | 0 .../data/themes/Light/media/media_previous.svg | 0 .../data/themes/Light/media/media_restart.svg | 0 .../data/themes/Light/media/media_stop.svg | 0 {UI => frontend}/data/themes/Light/minus.svg | 0 {UI => frontend}/data/themes/Light/mute.svg | 0 {UI => frontend}/data/themes/Light/no_sources.svg | 0 {UI => frontend}/data/themes/Light/plus.svg | 0 {UI => frontend}/data/themes/Light/popout.svg | 0 {UI => frontend}/data/themes/Light/refresh.svg | 0 {UI => frontend}/data/themes/Light/revert.svg | 0 {UI => frontend}/data/themes/Light/right.svg | 0 {UI => frontend}/data/themes/Light/save.svg | 0 .../data/themes/Light/settings/accessibility.svg | 0 .../data/themes/Light/settings/advanced.svg | 0 .../data/themes/Light/settings/appearance.svg | 0 .../data/themes/Light/settings/audio.svg | 0 .../data/themes/Light/settings/general.svg | 0 .../data/themes/Light/settings/hotkeys.svg | 0 .../data/themes/Light/settings/output.svg | 0 .../data/themes/Light/settings/stream.svg | 0 .../data/themes/Light/settings/video.svg | 0 .../data/themes/Light/sources/brush.svg | 0 .../data/themes/Light/sources/camera.svg | 0 .../data/themes/Light/sources/default.svg | 0 .../data/themes/Light/sources/gamepad.svg | 0 .../data/themes/Light/sources/globe.svg | 0 .../data/themes/Light/sources/group.svg | 0 .../data/themes/Light/sources/image.svg | 0 .../data/themes/Light/sources/media.svg | 0 .../data/themes/Light/sources/microphone.svg | 0 .../data/themes/Light/sources/scene.svg | 0 .../data/themes/Light/sources/slideshow.svg | 0 {UI => frontend}/data/themes/Light/sources/text.svg | 0 .../data/themes/Light/sources/window.svg | 0 .../data/themes/Light/sources/windowaudio.svg | 0 {UI => frontend}/data/themes/Light/trash.svg | 0 {UI => frontend}/data/themes/Light/up.svg | 0 {UI => frontend}/data/themes/Light/updown.svg | 0 {UI => frontend}/data/themes/Light/visible.svg | 0 .../data/themes/Rachni/checkbox_checked.png | Bin .../themes/Rachni/checkbox_checked_disabled.png | Bin .../data/themes/Rachni/checkbox_checked_focus.png | Bin .../data/themes/Rachni/checkbox_unchecked.png | Bin .../themes/Rachni/checkbox_unchecked_disabled.png | Bin .../data/themes/Rachni/checkbox_unchecked_focus.png | Bin {UI => frontend}/data/themes/Rachni/down_arrow.png | Bin .../data/themes/Rachni/down_arrow_disabled.png | Bin {UI => frontend}/data/themes/Rachni/left_arrow.png | Bin .../data/themes/Rachni/left_arrow_disabled.png | Bin .../data/themes/Rachni/radio_checked.png | Bin .../data/themes/Rachni/radio_checked_disabled.png | Bin .../data/themes/Rachni/radio_checked_focus.png | Bin .../data/themes/Rachni/radio_unchecked.png | Bin .../data/themes/Rachni/radio_unchecked_disabled.png | Bin .../data/themes/Rachni/radio_unchecked_focus.png | Bin {UI => frontend}/data/themes/Rachni/right_arrow.png | Bin .../data/themes/Rachni/right_arrow_disabled.png | Bin {UI => frontend}/data/themes/Rachni/sizegrip.png | Bin {UI => frontend}/data/themes/Rachni/up_arrow.png | Bin .../data/themes/Rachni/up_arrow_disabled.png | Bin {UI => frontend}/data/themes/System.obt | 0 {UI => frontend}/data/themes/Yami.obt | 0 .../data/themes/Yami/checkbox_checked.svg | 0 .../data/themes/Yami/checkbox_checked_disabled.svg | 0 .../data/themes/Yami/checkbox_checked_focus.svg | 0 .../data/themes/Yami/checkbox_unchecked.svg | 0 .../themes/Yami/checkbox_unchecked_disabled.svg | 0 .../data/themes/Yami/checkbox_unchecked_focus.svg | 0 {UI => frontend}/data/themes/Yami_Acri.ovt | 0 {UI => frontend}/data/themes/Yami_Classic.ovt | 0 {UI => frontend}/data/themes/Yami_Default.ovt | 0 {UI => frontend}/data/themes/Yami_Grey.ovt | 0 {UI => frontend}/data/themes/Yami_Light.ovt | 0 {UI => frontend}/data/themes/Yami_Rachni.ovt | 0 254 files changed, 0 insertions(+), 0 deletions(-) rename {UI => frontend}/data/OBSPublicRSAKey.pem (100%) rename {UI => frontend}/data/images/overflow.png (100%) rename {UI => frontend}/data/license/gplv2.txt (100%) rename {UI => frontend}/data/locale.ini (100%) rename {UI => frontend}/data/locale/af-ZA.ini (100%) rename {UI => frontend}/data/locale/an-ES.ini (100%) rename {UI => frontend}/data/locale/ar-SA.ini (100%) rename {UI => frontend}/data/locale/az-AZ.ini (100%) rename {UI => frontend}/data/locale/ba-RU.ini (100%) rename {UI => frontend}/data/locale/be-BY.ini (100%) rename {UI => frontend}/data/locale/bem-ZM.ini (100%) rename {UI => frontend}/data/locale/bg-BG.ini (100%) rename {UI => frontend}/data/locale/bn-BD.ini (100%) rename {UI => frontend}/data/locale/ca-ES.ini (100%) rename {UI => frontend}/data/locale/cs-CZ.ini (100%) rename {UI => frontend}/data/locale/da-DK.ini (100%) rename {UI => frontend}/data/locale/de-DE.ini (100%) rename {UI => frontend}/data/locale/el-GR.ini (100%) rename {UI => frontend}/data/locale/en-GB.ini (100%) rename {UI => frontend}/data/locale/en-US.ini (100%) rename {UI => frontend}/data/locale/eo-UY.ini (100%) rename {UI => frontend}/data/locale/es-ES.ini (100%) rename {UI => frontend}/data/locale/et-EE.ini (100%) rename {UI => frontend}/data/locale/eu-ES.ini (100%) rename {UI => frontend}/data/locale/fa-IR.ini (100%) rename {UI => frontend}/data/locale/fi-FI.ini (100%) rename {UI => frontend}/data/locale/fil-PH.ini (100%) rename {UI => frontend}/data/locale/fr-FR.ini (100%) rename {UI => frontend}/data/locale/gd-GB.ini (100%) rename {UI => frontend}/data/locale/gl-ES.ini (100%) rename {UI => frontend}/data/locale/he-IL.ini (100%) rename {UI => frontend}/data/locale/hi-IN.ini (100%) rename {UI => frontend}/data/locale/hr-HR.ini (100%) rename {UI => frontend}/data/locale/hu-HU.ini (100%) rename {UI => frontend}/data/locale/hy-AM.ini (100%) rename {UI => frontend}/data/locale/id-ID.ini (100%) rename {UI => frontend}/data/locale/is-IS.ini (100%) rename {UI => frontend}/data/locale/it-IT.ini (100%) rename {UI => frontend}/data/locale/ja-JP.ini (100%) rename {UI => frontend}/data/locale/ka-GE.ini (100%) rename {UI => frontend}/data/locale/kaa.ini (100%) rename {UI => frontend}/data/locale/kab-KAB.ini (100%) rename {UI => frontend}/data/locale/kmr-TR.ini (100%) rename {UI => frontend}/data/locale/ko-KR.ini (100%) rename {UI => frontend}/data/locale/lo-LA.ini (100%) rename {UI => frontend}/data/locale/lt-LT.ini (100%) rename {UI => frontend}/data/locale/lv-LV.ini (100%) rename {UI => frontend}/data/locale/mn-MN.ini (100%) rename {UI => frontend}/data/locale/ms-MY.ini (100%) rename {UI => frontend}/data/locale/nb-NO.ini (100%) rename {UI => frontend}/data/locale/nl-NL.ini (100%) rename {UI => frontend}/data/locale/nn-NO.ini (100%) rename {UI => frontend}/data/locale/oc-FR.ini (100%) rename {UI => frontend}/data/locale/pa-IN.ini (100%) rename {UI => frontend}/data/locale/pl-PL.ini (100%) rename {UI => frontend}/data/locale/pt-BR.ini (100%) rename {UI => frontend}/data/locale/pt-PT.ini (100%) rename {UI => frontend}/data/locale/ro-RO.ini (100%) rename {UI => frontend}/data/locale/ru-RU.ini (100%) rename {UI => frontend}/data/locale/si-LK.ini (100%) rename {UI => frontend}/data/locale/sk-SK.ini (100%) rename {UI => frontend}/data/locale/sl-SI.ini (100%) rename {UI => frontend}/data/locale/sq-AL.ini (100%) rename {UI => frontend}/data/locale/sr-CS.ini (100%) rename {UI => frontend}/data/locale/sr-SP.ini (100%) rename {UI => frontend}/data/locale/sv-SE.ini (100%) rename {UI => frontend}/data/locale/szl-PL.ini (100%) rename {UI => frontend}/data/locale/ta-IN.ini (100%) rename {UI => frontend}/data/locale/te-IN.ini (100%) rename {UI => frontend}/data/locale/th-TH.ini (100%) rename {UI => frontend}/data/locale/tl-PH.ini (100%) rename {UI => frontend}/data/locale/tr-TR.ini (100%) rename {UI => frontend}/data/locale/tt-RU.ini (100%) rename {UI => frontend}/data/locale/ug-CN.ini (100%) rename {UI => frontend}/data/locale/uk-UA.ini (100%) rename {UI => frontend}/data/locale/ur-PK.ini (100%) rename {UI => frontend}/data/locale/vi-VN.ini (100%) rename {UI => frontend}/data/locale/zh-CN.ini (100%) rename {UI => frontend}/data/locale/zh-TW.ini (100%) rename {UI => frontend}/data/themes/Acri/bot_hook.png (100%) rename {UI => frontend}/data/themes/Acri/bot_hook2.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_checked.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_checked_disabled.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_checked_focus.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_unchecked.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_unchecked_disabled.png (100%) rename {UI => frontend}/data/themes/Acri/checkbox_unchecked_focus.png (100%) rename {UI => frontend}/data/themes/Acri/radio_checked.png (100%) rename {UI => frontend}/data/themes/Acri/radio_checked_disabled.png (100%) rename {UI => frontend}/data/themes/Acri/radio_checked_focus.png (100%) rename {UI => frontend}/data/themes/Acri/radio_unchecked.png (100%) rename {UI => frontend}/data/themes/Acri/radio_unchecked_disabled.png (100%) rename {UI => frontend}/data/themes/Acri/radio_unchecked_focus.png (100%) rename {UI => frontend}/data/themes/Acri/sizegrip.png (100%) rename {UI => frontend}/data/themes/Acri/top_hook.png (100%) rename {UI => frontend}/data/themes/Dark/alert.svg (100%) rename {UI => frontend}/data/themes/Dark/close.svg (100%) rename {UI => frontend}/data/themes/Dark/cogs.svg (100%) rename {UI => frontend}/data/themes/Dark/collapse.svg (100%) rename {UI => frontend}/data/themes/Dark/dots-vert.svg (100%) rename {UI => frontend}/data/themes/Dark/dots.svg (100%) rename {UI => frontend}/data/themes/Dark/down.svg (100%) rename {UI => frontend}/data/themes/Dark/entry-clear.svg (100%) rename {UI => frontend}/data/themes/Dark/expand.svg (100%) rename {UI => frontend}/data/themes/Dark/filter.svg (100%) rename {UI => frontend}/data/themes/Dark/interact.svg (100%) rename {UI => frontend}/data/themes/Dark/left.svg (100%) rename {UI => frontend}/data/themes/Dark/locked.svg (100%) rename {UI => frontend}/data/themes/Dark/media-pause.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_next.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_pause.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_play.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_previous.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_restart.svg (100%) rename {UI => frontend}/data/themes/Dark/media/media_stop.svg (100%) rename {UI => frontend}/data/themes/Dark/minus.svg (100%) rename {UI => frontend}/data/themes/Dark/mute.svg (100%) rename {UI => frontend}/data/themes/Dark/network-disconnected.svg (100%) rename {UI => frontend}/data/themes/Dark/network-inactive.svg (100%) rename {UI => frontend}/data/themes/Dark/no_sources.svg (100%) rename {UI => frontend}/data/themes/Dark/plus.svg (100%) rename {UI => frontend}/data/themes/Dark/popout.svg (100%) rename {UI => frontend}/data/themes/Dark/recording-inactive.svg (100%) rename {UI => frontend}/data/themes/Dark/recording-pause-inactive.svg (100%) rename {UI => frontend}/data/themes/Dark/refresh.svg (100%) rename {UI => frontend}/data/themes/Dark/revert.svg (100%) rename {UI => frontend}/data/themes/Dark/right.svg (100%) rename {UI => frontend}/data/themes/Dark/save.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/accessibility.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/advanced.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/appearance.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/audio.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/general.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/hotkeys.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/output.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/stream.svg (100%) rename {UI => frontend}/data/themes/Dark/settings/video.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/brush.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/camera.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/default.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/gamepad.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/globe.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/group.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/image.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/media.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/microphone.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/scene.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/slideshow.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/text.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/window.svg (100%) rename {UI => frontend}/data/themes/Dark/sources/windowaudio.svg (100%) rename {UI => frontend}/data/themes/Dark/streaming-inactive.svg (100%) rename {UI => frontend}/data/themes/Dark/trash.svg (100%) rename {UI => frontend}/data/themes/Dark/unassigned.svg (100%) rename {UI => frontend}/data/themes/Dark/up.svg (100%) rename {UI => frontend}/data/themes/Dark/updown.svg (100%) rename {UI => frontend}/data/themes/Dark/visible.svg (100%) rename {UI => frontend}/data/themes/Light/alert.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_checked.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_checked_disabled.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_checked_focus.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_unchecked.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_unchecked_disabled.svg (100%) rename {UI => frontend}/data/themes/Light/checkbox_unchecked_focus.svg (100%) rename {UI => frontend}/data/themes/Light/close.svg (100%) rename {UI => frontend}/data/themes/Light/cogs.svg (100%) rename {UI => frontend}/data/themes/Light/collapse.svg (100%) rename {UI => frontend}/data/themes/Light/dots-vert.svg (100%) rename {UI => frontend}/data/themes/Light/dots.svg (100%) rename {UI => frontend}/data/themes/Light/down.svg (100%) rename {UI => frontend}/data/themes/Light/entry-clear.svg (100%) rename {UI => frontend}/data/themes/Light/expand.svg (100%) rename {UI => frontend}/data/themes/Light/filter.svg (100%) rename {UI => frontend}/data/themes/Light/interact.svg (100%) rename {UI => frontend}/data/themes/Light/left.svg (100%) rename {UI => frontend}/data/themes/Light/locked.svg (100%) rename {UI => frontend}/data/themes/Light/media-pause.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_next.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_pause.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_play.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_previous.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_restart.svg (100%) rename {UI => frontend}/data/themes/Light/media/media_stop.svg (100%) rename {UI => frontend}/data/themes/Light/minus.svg (100%) rename {UI => frontend}/data/themes/Light/mute.svg (100%) rename {UI => frontend}/data/themes/Light/no_sources.svg (100%) rename {UI => frontend}/data/themes/Light/plus.svg (100%) rename {UI => frontend}/data/themes/Light/popout.svg (100%) rename {UI => frontend}/data/themes/Light/refresh.svg (100%) rename {UI => frontend}/data/themes/Light/revert.svg (100%) rename {UI => frontend}/data/themes/Light/right.svg (100%) rename {UI => frontend}/data/themes/Light/save.svg (100%) rename {UI => frontend}/data/themes/Light/settings/accessibility.svg (100%) rename {UI => frontend}/data/themes/Light/settings/advanced.svg (100%) rename {UI => frontend}/data/themes/Light/settings/appearance.svg (100%) rename {UI => frontend}/data/themes/Light/settings/audio.svg (100%) rename {UI => frontend}/data/themes/Light/settings/general.svg (100%) rename {UI => frontend}/data/themes/Light/settings/hotkeys.svg (100%) rename {UI => frontend}/data/themes/Light/settings/output.svg (100%) rename {UI => frontend}/data/themes/Light/settings/stream.svg (100%) rename {UI => frontend}/data/themes/Light/settings/video.svg (100%) rename {UI => frontend}/data/themes/Light/sources/brush.svg (100%) rename {UI => frontend}/data/themes/Light/sources/camera.svg (100%) rename {UI => frontend}/data/themes/Light/sources/default.svg (100%) rename {UI => frontend}/data/themes/Light/sources/gamepad.svg (100%) rename {UI => frontend}/data/themes/Light/sources/globe.svg (100%) rename {UI => frontend}/data/themes/Light/sources/group.svg (100%) rename {UI => frontend}/data/themes/Light/sources/image.svg (100%) rename {UI => frontend}/data/themes/Light/sources/media.svg (100%) rename {UI => frontend}/data/themes/Light/sources/microphone.svg (100%) rename {UI => frontend}/data/themes/Light/sources/scene.svg (100%) rename {UI => frontend}/data/themes/Light/sources/slideshow.svg (100%) rename {UI => frontend}/data/themes/Light/sources/text.svg (100%) rename {UI => frontend}/data/themes/Light/sources/window.svg (100%) rename {UI => frontend}/data/themes/Light/sources/windowaudio.svg (100%) rename {UI => frontend}/data/themes/Light/trash.svg (100%) rename {UI => frontend}/data/themes/Light/up.svg (100%) rename {UI => frontend}/data/themes/Light/updown.svg (100%) rename {UI => frontend}/data/themes/Light/visible.svg (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_checked.png (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_checked_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_checked_focus.png (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_unchecked.png (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_unchecked_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/checkbox_unchecked_focus.png (100%) rename {UI => frontend}/data/themes/Rachni/down_arrow.png (100%) rename {UI => frontend}/data/themes/Rachni/down_arrow_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/left_arrow.png (100%) rename {UI => frontend}/data/themes/Rachni/left_arrow_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_checked.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_checked_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_checked_focus.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_unchecked.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_unchecked_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/radio_unchecked_focus.png (100%) rename {UI => frontend}/data/themes/Rachni/right_arrow.png (100%) rename {UI => frontend}/data/themes/Rachni/right_arrow_disabled.png (100%) rename {UI => frontend}/data/themes/Rachni/sizegrip.png (100%) rename {UI => frontend}/data/themes/Rachni/up_arrow.png (100%) rename {UI => frontend}/data/themes/Rachni/up_arrow_disabled.png (100%) rename {UI => frontend}/data/themes/System.obt (100%) rename {UI => frontend}/data/themes/Yami.obt (100%) rename {UI => frontend}/data/themes/Yami/checkbox_checked.svg (100%) rename {UI => frontend}/data/themes/Yami/checkbox_checked_disabled.svg (100%) rename {UI => frontend}/data/themes/Yami/checkbox_checked_focus.svg (100%) rename {UI => frontend}/data/themes/Yami/checkbox_unchecked.svg (100%) rename {UI => frontend}/data/themes/Yami/checkbox_unchecked_disabled.svg (100%) rename {UI => frontend}/data/themes/Yami/checkbox_unchecked_focus.svg (100%) rename {UI => frontend}/data/themes/Yami_Acri.ovt (100%) rename {UI => frontend}/data/themes/Yami_Classic.ovt (100%) rename {UI => frontend}/data/themes/Yami_Default.ovt (100%) rename {UI => frontend}/data/themes/Yami_Grey.ovt (100%) rename {UI => frontend}/data/themes/Yami_Light.ovt (100%) rename {UI => frontend}/data/themes/Yami_Rachni.ovt (100%) diff --git a/UI/data/OBSPublicRSAKey.pem b/frontend/data/OBSPublicRSAKey.pem similarity index 100% rename from UI/data/OBSPublicRSAKey.pem rename to frontend/data/OBSPublicRSAKey.pem diff --git a/UI/data/images/overflow.png b/frontend/data/images/overflow.png similarity index 100% rename from UI/data/images/overflow.png rename to frontend/data/images/overflow.png diff --git a/UI/data/license/gplv2.txt b/frontend/data/license/gplv2.txt similarity index 100% rename from UI/data/license/gplv2.txt rename to frontend/data/license/gplv2.txt diff --git a/UI/data/locale.ini b/frontend/data/locale.ini similarity index 100% rename from UI/data/locale.ini rename to frontend/data/locale.ini diff --git a/UI/data/locale/af-ZA.ini b/frontend/data/locale/af-ZA.ini similarity index 100% rename from UI/data/locale/af-ZA.ini rename to frontend/data/locale/af-ZA.ini diff --git a/UI/data/locale/an-ES.ini b/frontend/data/locale/an-ES.ini similarity index 100% rename from UI/data/locale/an-ES.ini rename to frontend/data/locale/an-ES.ini diff --git a/UI/data/locale/ar-SA.ini b/frontend/data/locale/ar-SA.ini similarity index 100% rename from UI/data/locale/ar-SA.ini rename to frontend/data/locale/ar-SA.ini diff --git a/UI/data/locale/az-AZ.ini b/frontend/data/locale/az-AZ.ini similarity index 100% rename from UI/data/locale/az-AZ.ini rename to frontend/data/locale/az-AZ.ini diff --git a/UI/data/locale/ba-RU.ini b/frontend/data/locale/ba-RU.ini similarity index 100% rename from UI/data/locale/ba-RU.ini rename to frontend/data/locale/ba-RU.ini diff --git a/UI/data/locale/be-BY.ini b/frontend/data/locale/be-BY.ini similarity index 100% rename from UI/data/locale/be-BY.ini rename to frontend/data/locale/be-BY.ini diff --git a/UI/data/locale/bem-ZM.ini b/frontend/data/locale/bem-ZM.ini similarity index 100% rename from UI/data/locale/bem-ZM.ini rename to frontend/data/locale/bem-ZM.ini diff --git a/UI/data/locale/bg-BG.ini b/frontend/data/locale/bg-BG.ini similarity index 100% rename from UI/data/locale/bg-BG.ini rename to frontend/data/locale/bg-BG.ini diff --git a/UI/data/locale/bn-BD.ini b/frontend/data/locale/bn-BD.ini similarity index 100% rename from UI/data/locale/bn-BD.ini rename to frontend/data/locale/bn-BD.ini diff --git a/UI/data/locale/ca-ES.ini b/frontend/data/locale/ca-ES.ini similarity index 100% rename from UI/data/locale/ca-ES.ini rename to frontend/data/locale/ca-ES.ini diff --git a/UI/data/locale/cs-CZ.ini b/frontend/data/locale/cs-CZ.ini similarity index 100% rename from UI/data/locale/cs-CZ.ini rename to frontend/data/locale/cs-CZ.ini diff --git a/UI/data/locale/da-DK.ini b/frontend/data/locale/da-DK.ini similarity index 100% rename from UI/data/locale/da-DK.ini rename to frontend/data/locale/da-DK.ini diff --git a/UI/data/locale/de-DE.ini b/frontend/data/locale/de-DE.ini similarity index 100% rename from UI/data/locale/de-DE.ini rename to frontend/data/locale/de-DE.ini diff --git a/UI/data/locale/el-GR.ini b/frontend/data/locale/el-GR.ini similarity index 100% rename from UI/data/locale/el-GR.ini rename to frontend/data/locale/el-GR.ini diff --git a/UI/data/locale/en-GB.ini b/frontend/data/locale/en-GB.ini similarity index 100% rename from UI/data/locale/en-GB.ini rename to frontend/data/locale/en-GB.ini diff --git a/UI/data/locale/en-US.ini b/frontend/data/locale/en-US.ini similarity index 100% rename from UI/data/locale/en-US.ini rename to frontend/data/locale/en-US.ini diff --git a/UI/data/locale/eo-UY.ini b/frontend/data/locale/eo-UY.ini similarity index 100% rename from UI/data/locale/eo-UY.ini rename to frontend/data/locale/eo-UY.ini diff --git a/UI/data/locale/es-ES.ini b/frontend/data/locale/es-ES.ini similarity index 100% rename from UI/data/locale/es-ES.ini rename to frontend/data/locale/es-ES.ini diff --git a/UI/data/locale/et-EE.ini b/frontend/data/locale/et-EE.ini similarity index 100% rename from UI/data/locale/et-EE.ini rename to frontend/data/locale/et-EE.ini diff --git a/UI/data/locale/eu-ES.ini b/frontend/data/locale/eu-ES.ini similarity index 100% rename from UI/data/locale/eu-ES.ini rename to frontend/data/locale/eu-ES.ini diff --git a/UI/data/locale/fa-IR.ini b/frontend/data/locale/fa-IR.ini similarity index 100% rename from UI/data/locale/fa-IR.ini rename to frontend/data/locale/fa-IR.ini diff --git a/UI/data/locale/fi-FI.ini b/frontend/data/locale/fi-FI.ini similarity index 100% rename from UI/data/locale/fi-FI.ini rename to frontend/data/locale/fi-FI.ini diff --git a/UI/data/locale/fil-PH.ini b/frontend/data/locale/fil-PH.ini similarity index 100% rename from UI/data/locale/fil-PH.ini rename to frontend/data/locale/fil-PH.ini diff --git a/UI/data/locale/fr-FR.ini b/frontend/data/locale/fr-FR.ini similarity index 100% rename from UI/data/locale/fr-FR.ini rename to frontend/data/locale/fr-FR.ini diff --git a/UI/data/locale/gd-GB.ini b/frontend/data/locale/gd-GB.ini similarity index 100% rename from UI/data/locale/gd-GB.ini rename to frontend/data/locale/gd-GB.ini diff --git a/UI/data/locale/gl-ES.ini b/frontend/data/locale/gl-ES.ini similarity index 100% rename from UI/data/locale/gl-ES.ini rename to frontend/data/locale/gl-ES.ini diff --git a/UI/data/locale/he-IL.ini b/frontend/data/locale/he-IL.ini similarity index 100% rename from UI/data/locale/he-IL.ini rename to frontend/data/locale/he-IL.ini diff --git a/UI/data/locale/hi-IN.ini b/frontend/data/locale/hi-IN.ini similarity index 100% rename from UI/data/locale/hi-IN.ini rename to frontend/data/locale/hi-IN.ini diff --git a/UI/data/locale/hr-HR.ini b/frontend/data/locale/hr-HR.ini similarity index 100% rename from UI/data/locale/hr-HR.ini rename to frontend/data/locale/hr-HR.ini diff --git a/UI/data/locale/hu-HU.ini b/frontend/data/locale/hu-HU.ini similarity index 100% rename from UI/data/locale/hu-HU.ini rename to frontend/data/locale/hu-HU.ini diff --git a/UI/data/locale/hy-AM.ini b/frontend/data/locale/hy-AM.ini similarity index 100% rename from UI/data/locale/hy-AM.ini rename to frontend/data/locale/hy-AM.ini diff --git a/UI/data/locale/id-ID.ini b/frontend/data/locale/id-ID.ini similarity index 100% rename from UI/data/locale/id-ID.ini rename to frontend/data/locale/id-ID.ini diff --git a/UI/data/locale/is-IS.ini b/frontend/data/locale/is-IS.ini similarity index 100% rename from UI/data/locale/is-IS.ini rename to frontend/data/locale/is-IS.ini diff --git a/UI/data/locale/it-IT.ini b/frontend/data/locale/it-IT.ini similarity index 100% rename from UI/data/locale/it-IT.ini rename to frontend/data/locale/it-IT.ini diff --git a/UI/data/locale/ja-JP.ini b/frontend/data/locale/ja-JP.ini similarity index 100% rename from UI/data/locale/ja-JP.ini rename to frontend/data/locale/ja-JP.ini diff --git a/UI/data/locale/ka-GE.ini b/frontend/data/locale/ka-GE.ini similarity index 100% rename from UI/data/locale/ka-GE.ini rename to frontend/data/locale/ka-GE.ini diff --git a/UI/data/locale/kaa.ini b/frontend/data/locale/kaa.ini similarity index 100% rename from UI/data/locale/kaa.ini rename to frontend/data/locale/kaa.ini diff --git a/UI/data/locale/kab-KAB.ini b/frontend/data/locale/kab-KAB.ini similarity index 100% rename from UI/data/locale/kab-KAB.ini rename to frontend/data/locale/kab-KAB.ini diff --git a/UI/data/locale/kmr-TR.ini b/frontend/data/locale/kmr-TR.ini similarity index 100% rename from UI/data/locale/kmr-TR.ini rename to frontend/data/locale/kmr-TR.ini diff --git a/UI/data/locale/ko-KR.ini b/frontend/data/locale/ko-KR.ini similarity index 100% rename from UI/data/locale/ko-KR.ini rename to frontend/data/locale/ko-KR.ini diff --git a/UI/data/locale/lo-LA.ini b/frontend/data/locale/lo-LA.ini similarity index 100% rename from UI/data/locale/lo-LA.ini rename to frontend/data/locale/lo-LA.ini diff --git a/UI/data/locale/lt-LT.ini b/frontend/data/locale/lt-LT.ini similarity index 100% rename from UI/data/locale/lt-LT.ini rename to frontend/data/locale/lt-LT.ini diff --git a/UI/data/locale/lv-LV.ini b/frontend/data/locale/lv-LV.ini similarity index 100% rename from UI/data/locale/lv-LV.ini rename to frontend/data/locale/lv-LV.ini diff --git a/UI/data/locale/mn-MN.ini b/frontend/data/locale/mn-MN.ini similarity index 100% rename from UI/data/locale/mn-MN.ini rename to frontend/data/locale/mn-MN.ini diff --git a/UI/data/locale/ms-MY.ini b/frontend/data/locale/ms-MY.ini similarity index 100% rename from UI/data/locale/ms-MY.ini rename to frontend/data/locale/ms-MY.ini diff --git a/UI/data/locale/nb-NO.ini b/frontend/data/locale/nb-NO.ini similarity index 100% rename from UI/data/locale/nb-NO.ini rename to frontend/data/locale/nb-NO.ini diff --git a/UI/data/locale/nl-NL.ini b/frontend/data/locale/nl-NL.ini similarity index 100% rename from UI/data/locale/nl-NL.ini rename to frontend/data/locale/nl-NL.ini diff --git a/UI/data/locale/nn-NO.ini b/frontend/data/locale/nn-NO.ini similarity index 100% rename from UI/data/locale/nn-NO.ini rename to frontend/data/locale/nn-NO.ini diff --git a/UI/data/locale/oc-FR.ini b/frontend/data/locale/oc-FR.ini similarity index 100% rename from UI/data/locale/oc-FR.ini rename to frontend/data/locale/oc-FR.ini diff --git a/UI/data/locale/pa-IN.ini b/frontend/data/locale/pa-IN.ini similarity index 100% rename from UI/data/locale/pa-IN.ini rename to frontend/data/locale/pa-IN.ini diff --git a/UI/data/locale/pl-PL.ini b/frontend/data/locale/pl-PL.ini similarity index 100% rename from UI/data/locale/pl-PL.ini rename to frontend/data/locale/pl-PL.ini diff --git a/UI/data/locale/pt-BR.ini b/frontend/data/locale/pt-BR.ini similarity index 100% rename from UI/data/locale/pt-BR.ini rename to frontend/data/locale/pt-BR.ini diff --git a/UI/data/locale/pt-PT.ini b/frontend/data/locale/pt-PT.ini similarity index 100% rename from UI/data/locale/pt-PT.ini rename to frontend/data/locale/pt-PT.ini diff --git a/UI/data/locale/ro-RO.ini b/frontend/data/locale/ro-RO.ini similarity index 100% rename from UI/data/locale/ro-RO.ini rename to frontend/data/locale/ro-RO.ini diff --git a/UI/data/locale/ru-RU.ini b/frontend/data/locale/ru-RU.ini similarity index 100% rename from UI/data/locale/ru-RU.ini rename to frontend/data/locale/ru-RU.ini diff --git a/UI/data/locale/si-LK.ini b/frontend/data/locale/si-LK.ini similarity index 100% rename from UI/data/locale/si-LK.ini rename to frontend/data/locale/si-LK.ini diff --git a/UI/data/locale/sk-SK.ini b/frontend/data/locale/sk-SK.ini similarity index 100% rename from UI/data/locale/sk-SK.ini rename to frontend/data/locale/sk-SK.ini diff --git a/UI/data/locale/sl-SI.ini b/frontend/data/locale/sl-SI.ini similarity index 100% rename from UI/data/locale/sl-SI.ini rename to frontend/data/locale/sl-SI.ini diff --git a/UI/data/locale/sq-AL.ini b/frontend/data/locale/sq-AL.ini similarity index 100% rename from UI/data/locale/sq-AL.ini rename to frontend/data/locale/sq-AL.ini diff --git a/UI/data/locale/sr-CS.ini b/frontend/data/locale/sr-CS.ini similarity index 100% rename from UI/data/locale/sr-CS.ini rename to frontend/data/locale/sr-CS.ini diff --git a/UI/data/locale/sr-SP.ini b/frontend/data/locale/sr-SP.ini similarity index 100% rename from UI/data/locale/sr-SP.ini rename to frontend/data/locale/sr-SP.ini diff --git a/UI/data/locale/sv-SE.ini b/frontend/data/locale/sv-SE.ini similarity index 100% rename from UI/data/locale/sv-SE.ini rename to frontend/data/locale/sv-SE.ini diff --git a/UI/data/locale/szl-PL.ini b/frontend/data/locale/szl-PL.ini similarity index 100% rename from UI/data/locale/szl-PL.ini rename to frontend/data/locale/szl-PL.ini diff --git a/UI/data/locale/ta-IN.ini b/frontend/data/locale/ta-IN.ini similarity index 100% rename from UI/data/locale/ta-IN.ini rename to frontend/data/locale/ta-IN.ini diff --git a/UI/data/locale/te-IN.ini b/frontend/data/locale/te-IN.ini similarity index 100% rename from UI/data/locale/te-IN.ini rename to frontend/data/locale/te-IN.ini diff --git a/UI/data/locale/th-TH.ini b/frontend/data/locale/th-TH.ini similarity index 100% rename from UI/data/locale/th-TH.ini rename to frontend/data/locale/th-TH.ini diff --git a/UI/data/locale/tl-PH.ini b/frontend/data/locale/tl-PH.ini similarity index 100% rename from UI/data/locale/tl-PH.ini rename to frontend/data/locale/tl-PH.ini diff --git a/UI/data/locale/tr-TR.ini b/frontend/data/locale/tr-TR.ini similarity index 100% rename from UI/data/locale/tr-TR.ini rename to frontend/data/locale/tr-TR.ini diff --git a/UI/data/locale/tt-RU.ini b/frontend/data/locale/tt-RU.ini similarity index 100% rename from UI/data/locale/tt-RU.ini rename to frontend/data/locale/tt-RU.ini diff --git a/UI/data/locale/ug-CN.ini b/frontend/data/locale/ug-CN.ini similarity index 100% rename from UI/data/locale/ug-CN.ini rename to frontend/data/locale/ug-CN.ini diff --git a/UI/data/locale/uk-UA.ini b/frontend/data/locale/uk-UA.ini similarity index 100% rename from UI/data/locale/uk-UA.ini rename to frontend/data/locale/uk-UA.ini diff --git a/UI/data/locale/ur-PK.ini b/frontend/data/locale/ur-PK.ini similarity index 100% rename from UI/data/locale/ur-PK.ini rename to frontend/data/locale/ur-PK.ini diff --git a/UI/data/locale/vi-VN.ini b/frontend/data/locale/vi-VN.ini similarity index 100% rename from UI/data/locale/vi-VN.ini rename to frontend/data/locale/vi-VN.ini diff --git a/UI/data/locale/zh-CN.ini b/frontend/data/locale/zh-CN.ini similarity index 100% rename from UI/data/locale/zh-CN.ini rename to frontend/data/locale/zh-CN.ini diff --git a/UI/data/locale/zh-TW.ini b/frontend/data/locale/zh-TW.ini similarity index 100% rename from UI/data/locale/zh-TW.ini rename to frontend/data/locale/zh-TW.ini diff --git a/UI/data/themes/Acri/bot_hook.png b/frontend/data/themes/Acri/bot_hook.png similarity index 100% rename from UI/data/themes/Acri/bot_hook.png rename to frontend/data/themes/Acri/bot_hook.png diff --git a/UI/data/themes/Acri/bot_hook2.png b/frontend/data/themes/Acri/bot_hook2.png similarity index 100% rename from UI/data/themes/Acri/bot_hook2.png rename to frontend/data/themes/Acri/bot_hook2.png diff --git a/UI/data/themes/Acri/checkbox_checked.png b/frontend/data/themes/Acri/checkbox_checked.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked.png rename to frontend/data/themes/Acri/checkbox_checked.png diff --git a/UI/data/themes/Acri/checkbox_checked_disabled.png b/frontend/data/themes/Acri/checkbox_checked_disabled.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked_disabled.png rename to frontend/data/themes/Acri/checkbox_checked_disabled.png diff --git a/UI/data/themes/Acri/checkbox_checked_focus.png b/frontend/data/themes/Acri/checkbox_checked_focus.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked_focus.png rename to frontend/data/themes/Acri/checkbox_checked_focus.png diff --git a/UI/data/themes/Acri/checkbox_unchecked.png b/frontend/data/themes/Acri/checkbox_unchecked.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked.png rename to frontend/data/themes/Acri/checkbox_unchecked.png diff --git a/UI/data/themes/Acri/checkbox_unchecked_disabled.png b/frontend/data/themes/Acri/checkbox_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked_disabled.png rename to frontend/data/themes/Acri/checkbox_unchecked_disabled.png diff --git a/UI/data/themes/Acri/checkbox_unchecked_focus.png b/frontend/data/themes/Acri/checkbox_unchecked_focus.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked_focus.png rename to frontend/data/themes/Acri/checkbox_unchecked_focus.png diff --git a/UI/data/themes/Acri/radio_checked.png b/frontend/data/themes/Acri/radio_checked.png similarity index 100% rename from UI/data/themes/Acri/radio_checked.png rename to frontend/data/themes/Acri/radio_checked.png diff --git a/UI/data/themes/Acri/radio_checked_disabled.png b/frontend/data/themes/Acri/radio_checked_disabled.png similarity index 100% rename from UI/data/themes/Acri/radio_checked_disabled.png rename to frontend/data/themes/Acri/radio_checked_disabled.png diff --git a/UI/data/themes/Acri/radio_checked_focus.png b/frontend/data/themes/Acri/radio_checked_focus.png similarity index 100% rename from UI/data/themes/Acri/radio_checked_focus.png rename to frontend/data/themes/Acri/radio_checked_focus.png diff --git a/UI/data/themes/Acri/radio_unchecked.png b/frontend/data/themes/Acri/radio_unchecked.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked.png rename to frontend/data/themes/Acri/radio_unchecked.png diff --git a/UI/data/themes/Acri/radio_unchecked_disabled.png b/frontend/data/themes/Acri/radio_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked_disabled.png rename to frontend/data/themes/Acri/radio_unchecked_disabled.png diff --git a/UI/data/themes/Acri/radio_unchecked_focus.png b/frontend/data/themes/Acri/radio_unchecked_focus.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked_focus.png rename to frontend/data/themes/Acri/radio_unchecked_focus.png diff --git a/UI/data/themes/Acri/sizegrip.png b/frontend/data/themes/Acri/sizegrip.png similarity index 100% rename from UI/data/themes/Acri/sizegrip.png rename to frontend/data/themes/Acri/sizegrip.png diff --git a/UI/data/themes/Acri/top_hook.png b/frontend/data/themes/Acri/top_hook.png similarity index 100% rename from UI/data/themes/Acri/top_hook.png rename to frontend/data/themes/Acri/top_hook.png diff --git a/UI/data/themes/Dark/alert.svg b/frontend/data/themes/Dark/alert.svg similarity index 100% rename from UI/data/themes/Dark/alert.svg rename to frontend/data/themes/Dark/alert.svg diff --git a/UI/data/themes/Dark/close.svg b/frontend/data/themes/Dark/close.svg similarity index 100% rename from UI/data/themes/Dark/close.svg rename to frontend/data/themes/Dark/close.svg diff --git a/UI/data/themes/Dark/cogs.svg b/frontend/data/themes/Dark/cogs.svg similarity index 100% rename from UI/data/themes/Dark/cogs.svg rename to frontend/data/themes/Dark/cogs.svg diff --git a/UI/data/themes/Dark/collapse.svg b/frontend/data/themes/Dark/collapse.svg similarity index 100% rename from UI/data/themes/Dark/collapse.svg rename to frontend/data/themes/Dark/collapse.svg diff --git a/UI/data/themes/Dark/dots-vert.svg b/frontend/data/themes/Dark/dots-vert.svg similarity index 100% rename from UI/data/themes/Dark/dots-vert.svg rename to frontend/data/themes/Dark/dots-vert.svg diff --git a/UI/data/themes/Dark/dots.svg b/frontend/data/themes/Dark/dots.svg similarity index 100% rename from UI/data/themes/Dark/dots.svg rename to frontend/data/themes/Dark/dots.svg diff --git a/UI/data/themes/Dark/down.svg b/frontend/data/themes/Dark/down.svg similarity index 100% rename from UI/data/themes/Dark/down.svg rename to frontend/data/themes/Dark/down.svg diff --git a/UI/data/themes/Dark/entry-clear.svg b/frontend/data/themes/Dark/entry-clear.svg similarity index 100% rename from UI/data/themes/Dark/entry-clear.svg rename to frontend/data/themes/Dark/entry-clear.svg diff --git a/UI/data/themes/Dark/expand.svg b/frontend/data/themes/Dark/expand.svg similarity index 100% rename from UI/data/themes/Dark/expand.svg rename to frontend/data/themes/Dark/expand.svg diff --git a/UI/data/themes/Dark/filter.svg b/frontend/data/themes/Dark/filter.svg similarity index 100% rename from UI/data/themes/Dark/filter.svg rename to frontend/data/themes/Dark/filter.svg diff --git a/UI/data/themes/Dark/interact.svg b/frontend/data/themes/Dark/interact.svg similarity index 100% rename from UI/data/themes/Dark/interact.svg rename to frontend/data/themes/Dark/interact.svg diff --git a/UI/data/themes/Dark/left.svg b/frontend/data/themes/Dark/left.svg similarity index 100% rename from UI/data/themes/Dark/left.svg rename to frontend/data/themes/Dark/left.svg diff --git a/UI/data/themes/Dark/locked.svg b/frontend/data/themes/Dark/locked.svg similarity index 100% rename from UI/data/themes/Dark/locked.svg rename to frontend/data/themes/Dark/locked.svg diff --git a/UI/data/themes/Dark/media-pause.svg b/frontend/data/themes/Dark/media-pause.svg similarity index 100% rename from UI/data/themes/Dark/media-pause.svg rename to frontend/data/themes/Dark/media-pause.svg diff --git a/UI/data/themes/Dark/media/media_next.svg b/frontend/data/themes/Dark/media/media_next.svg similarity index 100% rename from UI/data/themes/Dark/media/media_next.svg rename to frontend/data/themes/Dark/media/media_next.svg diff --git a/UI/data/themes/Dark/media/media_pause.svg b/frontend/data/themes/Dark/media/media_pause.svg similarity index 100% rename from UI/data/themes/Dark/media/media_pause.svg rename to frontend/data/themes/Dark/media/media_pause.svg diff --git a/UI/data/themes/Dark/media/media_play.svg b/frontend/data/themes/Dark/media/media_play.svg similarity index 100% rename from UI/data/themes/Dark/media/media_play.svg rename to frontend/data/themes/Dark/media/media_play.svg diff --git a/UI/data/themes/Dark/media/media_previous.svg b/frontend/data/themes/Dark/media/media_previous.svg similarity index 100% rename from UI/data/themes/Dark/media/media_previous.svg rename to frontend/data/themes/Dark/media/media_previous.svg diff --git a/UI/data/themes/Dark/media/media_restart.svg b/frontend/data/themes/Dark/media/media_restart.svg similarity index 100% rename from UI/data/themes/Dark/media/media_restart.svg rename to frontend/data/themes/Dark/media/media_restart.svg diff --git a/UI/data/themes/Dark/media/media_stop.svg b/frontend/data/themes/Dark/media/media_stop.svg similarity index 100% rename from UI/data/themes/Dark/media/media_stop.svg rename to frontend/data/themes/Dark/media/media_stop.svg diff --git a/UI/data/themes/Dark/minus.svg b/frontend/data/themes/Dark/minus.svg similarity index 100% rename from UI/data/themes/Dark/minus.svg rename to frontend/data/themes/Dark/minus.svg diff --git a/UI/data/themes/Dark/mute.svg b/frontend/data/themes/Dark/mute.svg similarity index 100% rename from UI/data/themes/Dark/mute.svg rename to frontend/data/themes/Dark/mute.svg diff --git a/UI/data/themes/Dark/network-disconnected.svg b/frontend/data/themes/Dark/network-disconnected.svg similarity index 100% rename from UI/data/themes/Dark/network-disconnected.svg rename to frontend/data/themes/Dark/network-disconnected.svg diff --git a/UI/data/themes/Dark/network-inactive.svg b/frontend/data/themes/Dark/network-inactive.svg similarity index 100% rename from UI/data/themes/Dark/network-inactive.svg rename to frontend/data/themes/Dark/network-inactive.svg diff --git a/UI/data/themes/Dark/no_sources.svg b/frontend/data/themes/Dark/no_sources.svg similarity index 100% rename from UI/data/themes/Dark/no_sources.svg rename to frontend/data/themes/Dark/no_sources.svg diff --git a/UI/data/themes/Dark/plus.svg b/frontend/data/themes/Dark/plus.svg similarity index 100% rename from UI/data/themes/Dark/plus.svg rename to frontend/data/themes/Dark/plus.svg diff --git a/UI/data/themes/Dark/popout.svg b/frontend/data/themes/Dark/popout.svg similarity index 100% rename from UI/data/themes/Dark/popout.svg rename to frontend/data/themes/Dark/popout.svg diff --git a/UI/data/themes/Dark/recording-inactive.svg b/frontend/data/themes/Dark/recording-inactive.svg similarity index 100% rename from UI/data/themes/Dark/recording-inactive.svg rename to frontend/data/themes/Dark/recording-inactive.svg diff --git a/UI/data/themes/Dark/recording-pause-inactive.svg b/frontend/data/themes/Dark/recording-pause-inactive.svg similarity index 100% rename from UI/data/themes/Dark/recording-pause-inactive.svg rename to frontend/data/themes/Dark/recording-pause-inactive.svg diff --git a/UI/data/themes/Dark/refresh.svg b/frontend/data/themes/Dark/refresh.svg similarity index 100% rename from UI/data/themes/Dark/refresh.svg rename to frontend/data/themes/Dark/refresh.svg diff --git a/UI/data/themes/Dark/revert.svg b/frontend/data/themes/Dark/revert.svg similarity index 100% rename from UI/data/themes/Dark/revert.svg rename to frontend/data/themes/Dark/revert.svg diff --git a/UI/data/themes/Dark/right.svg b/frontend/data/themes/Dark/right.svg similarity index 100% rename from UI/data/themes/Dark/right.svg rename to frontend/data/themes/Dark/right.svg diff --git a/UI/data/themes/Dark/save.svg b/frontend/data/themes/Dark/save.svg similarity index 100% rename from UI/data/themes/Dark/save.svg rename to frontend/data/themes/Dark/save.svg diff --git a/UI/data/themes/Dark/settings/accessibility.svg b/frontend/data/themes/Dark/settings/accessibility.svg similarity index 100% rename from UI/data/themes/Dark/settings/accessibility.svg rename to frontend/data/themes/Dark/settings/accessibility.svg diff --git a/UI/data/themes/Dark/settings/advanced.svg b/frontend/data/themes/Dark/settings/advanced.svg similarity index 100% rename from UI/data/themes/Dark/settings/advanced.svg rename to frontend/data/themes/Dark/settings/advanced.svg diff --git a/UI/data/themes/Dark/settings/appearance.svg b/frontend/data/themes/Dark/settings/appearance.svg similarity index 100% rename from UI/data/themes/Dark/settings/appearance.svg rename to frontend/data/themes/Dark/settings/appearance.svg diff --git a/UI/data/themes/Dark/settings/audio.svg b/frontend/data/themes/Dark/settings/audio.svg similarity index 100% rename from UI/data/themes/Dark/settings/audio.svg rename to frontend/data/themes/Dark/settings/audio.svg diff --git a/UI/data/themes/Dark/settings/general.svg b/frontend/data/themes/Dark/settings/general.svg similarity index 100% rename from UI/data/themes/Dark/settings/general.svg rename to frontend/data/themes/Dark/settings/general.svg diff --git a/UI/data/themes/Dark/settings/hotkeys.svg b/frontend/data/themes/Dark/settings/hotkeys.svg similarity index 100% rename from UI/data/themes/Dark/settings/hotkeys.svg rename to frontend/data/themes/Dark/settings/hotkeys.svg diff --git a/UI/data/themes/Dark/settings/output.svg b/frontend/data/themes/Dark/settings/output.svg similarity index 100% rename from UI/data/themes/Dark/settings/output.svg rename to frontend/data/themes/Dark/settings/output.svg diff --git a/UI/data/themes/Dark/settings/stream.svg b/frontend/data/themes/Dark/settings/stream.svg similarity index 100% rename from UI/data/themes/Dark/settings/stream.svg rename to frontend/data/themes/Dark/settings/stream.svg diff --git a/UI/data/themes/Dark/settings/video.svg b/frontend/data/themes/Dark/settings/video.svg similarity index 100% rename from UI/data/themes/Dark/settings/video.svg rename to frontend/data/themes/Dark/settings/video.svg diff --git a/UI/data/themes/Dark/sources/brush.svg b/frontend/data/themes/Dark/sources/brush.svg similarity index 100% rename from UI/data/themes/Dark/sources/brush.svg rename to frontend/data/themes/Dark/sources/brush.svg diff --git a/UI/data/themes/Dark/sources/camera.svg b/frontend/data/themes/Dark/sources/camera.svg similarity index 100% rename from UI/data/themes/Dark/sources/camera.svg rename to frontend/data/themes/Dark/sources/camera.svg diff --git a/UI/data/themes/Dark/sources/default.svg b/frontend/data/themes/Dark/sources/default.svg similarity index 100% rename from UI/data/themes/Dark/sources/default.svg rename to frontend/data/themes/Dark/sources/default.svg diff --git a/UI/data/themes/Dark/sources/gamepad.svg b/frontend/data/themes/Dark/sources/gamepad.svg similarity index 100% rename from UI/data/themes/Dark/sources/gamepad.svg rename to frontend/data/themes/Dark/sources/gamepad.svg diff --git a/UI/data/themes/Dark/sources/globe.svg b/frontend/data/themes/Dark/sources/globe.svg similarity index 100% rename from UI/data/themes/Dark/sources/globe.svg rename to frontend/data/themes/Dark/sources/globe.svg diff --git a/UI/data/themes/Dark/sources/group.svg b/frontend/data/themes/Dark/sources/group.svg similarity index 100% rename from UI/data/themes/Dark/sources/group.svg rename to frontend/data/themes/Dark/sources/group.svg diff --git a/UI/data/themes/Dark/sources/image.svg b/frontend/data/themes/Dark/sources/image.svg similarity index 100% rename from UI/data/themes/Dark/sources/image.svg rename to frontend/data/themes/Dark/sources/image.svg diff --git a/UI/data/themes/Dark/sources/media.svg b/frontend/data/themes/Dark/sources/media.svg similarity index 100% rename from UI/data/themes/Dark/sources/media.svg rename to frontend/data/themes/Dark/sources/media.svg diff --git a/UI/data/themes/Dark/sources/microphone.svg b/frontend/data/themes/Dark/sources/microphone.svg similarity index 100% rename from UI/data/themes/Dark/sources/microphone.svg rename to frontend/data/themes/Dark/sources/microphone.svg diff --git a/UI/data/themes/Dark/sources/scene.svg b/frontend/data/themes/Dark/sources/scene.svg similarity index 100% rename from UI/data/themes/Dark/sources/scene.svg rename to frontend/data/themes/Dark/sources/scene.svg diff --git a/UI/data/themes/Dark/sources/slideshow.svg b/frontend/data/themes/Dark/sources/slideshow.svg similarity index 100% rename from UI/data/themes/Dark/sources/slideshow.svg rename to frontend/data/themes/Dark/sources/slideshow.svg diff --git a/UI/data/themes/Dark/sources/text.svg b/frontend/data/themes/Dark/sources/text.svg similarity index 100% rename from UI/data/themes/Dark/sources/text.svg rename to frontend/data/themes/Dark/sources/text.svg diff --git a/UI/data/themes/Dark/sources/window.svg b/frontend/data/themes/Dark/sources/window.svg similarity index 100% rename from UI/data/themes/Dark/sources/window.svg rename to frontend/data/themes/Dark/sources/window.svg diff --git a/UI/data/themes/Dark/sources/windowaudio.svg b/frontend/data/themes/Dark/sources/windowaudio.svg similarity index 100% rename from UI/data/themes/Dark/sources/windowaudio.svg rename to frontend/data/themes/Dark/sources/windowaudio.svg diff --git a/UI/data/themes/Dark/streaming-inactive.svg b/frontend/data/themes/Dark/streaming-inactive.svg similarity index 100% rename from UI/data/themes/Dark/streaming-inactive.svg rename to frontend/data/themes/Dark/streaming-inactive.svg diff --git a/UI/data/themes/Dark/trash.svg b/frontend/data/themes/Dark/trash.svg similarity index 100% rename from UI/data/themes/Dark/trash.svg rename to frontend/data/themes/Dark/trash.svg diff --git a/UI/data/themes/Dark/unassigned.svg b/frontend/data/themes/Dark/unassigned.svg similarity index 100% rename from UI/data/themes/Dark/unassigned.svg rename to frontend/data/themes/Dark/unassigned.svg diff --git a/UI/data/themes/Dark/up.svg b/frontend/data/themes/Dark/up.svg similarity index 100% rename from UI/data/themes/Dark/up.svg rename to frontend/data/themes/Dark/up.svg diff --git a/UI/data/themes/Dark/updown.svg b/frontend/data/themes/Dark/updown.svg similarity index 100% rename from UI/data/themes/Dark/updown.svg rename to frontend/data/themes/Dark/updown.svg diff --git a/UI/data/themes/Dark/visible.svg b/frontend/data/themes/Dark/visible.svg similarity index 100% rename from UI/data/themes/Dark/visible.svg rename to frontend/data/themes/Dark/visible.svg diff --git a/UI/data/themes/Light/alert.svg b/frontend/data/themes/Light/alert.svg similarity index 100% rename from UI/data/themes/Light/alert.svg rename to frontend/data/themes/Light/alert.svg diff --git a/UI/data/themes/Light/checkbox_checked.svg b/frontend/data/themes/Light/checkbox_checked.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked.svg rename to frontend/data/themes/Light/checkbox_checked.svg diff --git a/UI/data/themes/Light/checkbox_checked_disabled.svg b/frontend/data/themes/Light/checkbox_checked_disabled.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked_disabled.svg rename to frontend/data/themes/Light/checkbox_checked_disabled.svg diff --git a/UI/data/themes/Light/checkbox_checked_focus.svg b/frontend/data/themes/Light/checkbox_checked_focus.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked_focus.svg rename to frontend/data/themes/Light/checkbox_checked_focus.svg diff --git a/UI/data/themes/Light/checkbox_unchecked.svg b/frontend/data/themes/Light/checkbox_unchecked.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked.svg rename to frontend/data/themes/Light/checkbox_unchecked.svg diff --git a/UI/data/themes/Light/checkbox_unchecked_disabled.svg b/frontend/data/themes/Light/checkbox_unchecked_disabled.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked_disabled.svg rename to frontend/data/themes/Light/checkbox_unchecked_disabled.svg diff --git a/UI/data/themes/Light/checkbox_unchecked_focus.svg b/frontend/data/themes/Light/checkbox_unchecked_focus.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked_focus.svg rename to frontend/data/themes/Light/checkbox_unchecked_focus.svg diff --git a/UI/data/themes/Light/close.svg b/frontend/data/themes/Light/close.svg similarity index 100% rename from UI/data/themes/Light/close.svg rename to frontend/data/themes/Light/close.svg diff --git a/UI/data/themes/Light/cogs.svg b/frontend/data/themes/Light/cogs.svg similarity index 100% rename from UI/data/themes/Light/cogs.svg rename to frontend/data/themes/Light/cogs.svg diff --git a/UI/data/themes/Light/collapse.svg b/frontend/data/themes/Light/collapse.svg similarity index 100% rename from UI/data/themes/Light/collapse.svg rename to frontend/data/themes/Light/collapse.svg diff --git a/UI/data/themes/Light/dots-vert.svg b/frontend/data/themes/Light/dots-vert.svg similarity index 100% rename from UI/data/themes/Light/dots-vert.svg rename to frontend/data/themes/Light/dots-vert.svg diff --git a/UI/data/themes/Light/dots.svg b/frontend/data/themes/Light/dots.svg similarity index 100% rename from UI/data/themes/Light/dots.svg rename to frontend/data/themes/Light/dots.svg diff --git a/UI/data/themes/Light/down.svg b/frontend/data/themes/Light/down.svg similarity index 100% rename from UI/data/themes/Light/down.svg rename to frontend/data/themes/Light/down.svg diff --git a/UI/data/themes/Light/entry-clear.svg b/frontend/data/themes/Light/entry-clear.svg similarity index 100% rename from UI/data/themes/Light/entry-clear.svg rename to frontend/data/themes/Light/entry-clear.svg diff --git a/UI/data/themes/Light/expand.svg b/frontend/data/themes/Light/expand.svg similarity index 100% rename from UI/data/themes/Light/expand.svg rename to frontend/data/themes/Light/expand.svg diff --git a/UI/data/themes/Light/filter.svg b/frontend/data/themes/Light/filter.svg similarity index 100% rename from UI/data/themes/Light/filter.svg rename to frontend/data/themes/Light/filter.svg diff --git a/UI/data/themes/Light/interact.svg b/frontend/data/themes/Light/interact.svg similarity index 100% rename from UI/data/themes/Light/interact.svg rename to frontend/data/themes/Light/interact.svg diff --git a/UI/data/themes/Light/left.svg b/frontend/data/themes/Light/left.svg similarity index 100% rename from UI/data/themes/Light/left.svg rename to frontend/data/themes/Light/left.svg diff --git a/UI/data/themes/Light/locked.svg b/frontend/data/themes/Light/locked.svg similarity index 100% rename from UI/data/themes/Light/locked.svg rename to frontend/data/themes/Light/locked.svg diff --git a/UI/data/themes/Light/media-pause.svg b/frontend/data/themes/Light/media-pause.svg similarity index 100% rename from UI/data/themes/Light/media-pause.svg rename to frontend/data/themes/Light/media-pause.svg diff --git a/UI/data/themes/Light/media/media_next.svg b/frontend/data/themes/Light/media/media_next.svg similarity index 100% rename from UI/data/themes/Light/media/media_next.svg rename to frontend/data/themes/Light/media/media_next.svg diff --git a/UI/data/themes/Light/media/media_pause.svg b/frontend/data/themes/Light/media/media_pause.svg similarity index 100% rename from UI/data/themes/Light/media/media_pause.svg rename to frontend/data/themes/Light/media/media_pause.svg diff --git a/UI/data/themes/Light/media/media_play.svg b/frontend/data/themes/Light/media/media_play.svg similarity index 100% rename from UI/data/themes/Light/media/media_play.svg rename to frontend/data/themes/Light/media/media_play.svg diff --git a/UI/data/themes/Light/media/media_previous.svg b/frontend/data/themes/Light/media/media_previous.svg similarity index 100% rename from UI/data/themes/Light/media/media_previous.svg rename to frontend/data/themes/Light/media/media_previous.svg diff --git a/UI/data/themes/Light/media/media_restart.svg b/frontend/data/themes/Light/media/media_restart.svg similarity index 100% rename from UI/data/themes/Light/media/media_restart.svg rename to frontend/data/themes/Light/media/media_restart.svg diff --git a/UI/data/themes/Light/media/media_stop.svg b/frontend/data/themes/Light/media/media_stop.svg similarity index 100% rename from UI/data/themes/Light/media/media_stop.svg rename to frontend/data/themes/Light/media/media_stop.svg diff --git a/UI/data/themes/Light/minus.svg b/frontend/data/themes/Light/minus.svg similarity index 100% rename from UI/data/themes/Light/minus.svg rename to frontend/data/themes/Light/minus.svg diff --git a/UI/data/themes/Light/mute.svg b/frontend/data/themes/Light/mute.svg similarity index 100% rename from UI/data/themes/Light/mute.svg rename to frontend/data/themes/Light/mute.svg diff --git a/UI/data/themes/Light/no_sources.svg b/frontend/data/themes/Light/no_sources.svg similarity index 100% rename from UI/data/themes/Light/no_sources.svg rename to frontend/data/themes/Light/no_sources.svg diff --git a/UI/data/themes/Light/plus.svg b/frontend/data/themes/Light/plus.svg similarity index 100% rename from UI/data/themes/Light/plus.svg rename to frontend/data/themes/Light/plus.svg diff --git a/UI/data/themes/Light/popout.svg b/frontend/data/themes/Light/popout.svg similarity index 100% rename from UI/data/themes/Light/popout.svg rename to frontend/data/themes/Light/popout.svg diff --git a/UI/data/themes/Light/refresh.svg b/frontend/data/themes/Light/refresh.svg similarity index 100% rename from UI/data/themes/Light/refresh.svg rename to frontend/data/themes/Light/refresh.svg diff --git a/UI/data/themes/Light/revert.svg b/frontend/data/themes/Light/revert.svg similarity index 100% rename from UI/data/themes/Light/revert.svg rename to frontend/data/themes/Light/revert.svg diff --git a/UI/data/themes/Light/right.svg b/frontend/data/themes/Light/right.svg similarity index 100% rename from UI/data/themes/Light/right.svg rename to frontend/data/themes/Light/right.svg diff --git a/UI/data/themes/Light/save.svg b/frontend/data/themes/Light/save.svg similarity index 100% rename from UI/data/themes/Light/save.svg rename to frontend/data/themes/Light/save.svg diff --git a/UI/data/themes/Light/settings/accessibility.svg b/frontend/data/themes/Light/settings/accessibility.svg similarity index 100% rename from UI/data/themes/Light/settings/accessibility.svg rename to frontend/data/themes/Light/settings/accessibility.svg diff --git a/UI/data/themes/Light/settings/advanced.svg b/frontend/data/themes/Light/settings/advanced.svg similarity index 100% rename from UI/data/themes/Light/settings/advanced.svg rename to frontend/data/themes/Light/settings/advanced.svg diff --git a/UI/data/themes/Light/settings/appearance.svg b/frontend/data/themes/Light/settings/appearance.svg similarity index 100% rename from UI/data/themes/Light/settings/appearance.svg rename to frontend/data/themes/Light/settings/appearance.svg diff --git a/UI/data/themes/Light/settings/audio.svg b/frontend/data/themes/Light/settings/audio.svg similarity index 100% rename from UI/data/themes/Light/settings/audio.svg rename to frontend/data/themes/Light/settings/audio.svg diff --git a/UI/data/themes/Light/settings/general.svg b/frontend/data/themes/Light/settings/general.svg similarity index 100% rename from UI/data/themes/Light/settings/general.svg rename to frontend/data/themes/Light/settings/general.svg diff --git a/UI/data/themes/Light/settings/hotkeys.svg b/frontend/data/themes/Light/settings/hotkeys.svg similarity index 100% rename from UI/data/themes/Light/settings/hotkeys.svg rename to frontend/data/themes/Light/settings/hotkeys.svg diff --git a/UI/data/themes/Light/settings/output.svg b/frontend/data/themes/Light/settings/output.svg similarity index 100% rename from UI/data/themes/Light/settings/output.svg rename to frontend/data/themes/Light/settings/output.svg diff --git a/UI/data/themes/Light/settings/stream.svg b/frontend/data/themes/Light/settings/stream.svg similarity index 100% rename from UI/data/themes/Light/settings/stream.svg rename to frontend/data/themes/Light/settings/stream.svg diff --git a/UI/data/themes/Light/settings/video.svg b/frontend/data/themes/Light/settings/video.svg similarity index 100% rename from UI/data/themes/Light/settings/video.svg rename to frontend/data/themes/Light/settings/video.svg diff --git a/UI/data/themes/Light/sources/brush.svg b/frontend/data/themes/Light/sources/brush.svg similarity index 100% rename from UI/data/themes/Light/sources/brush.svg rename to frontend/data/themes/Light/sources/brush.svg diff --git a/UI/data/themes/Light/sources/camera.svg b/frontend/data/themes/Light/sources/camera.svg similarity index 100% rename from UI/data/themes/Light/sources/camera.svg rename to frontend/data/themes/Light/sources/camera.svg diff --git a/UI/data/themes/Light/sources/default.svg b/frontend/data/themes/Light/sources/default.svg similarity index 100% rename from UI/data/themes/Light/sources/default.svg rename to frontend/data/themes/Light/sources/default.svg diff --git a/UI/data/themes/Light/sources/gamepad.svg b/frontend/data/themes/Light/sources/gamepad.svg similarity index 100% rename from UI/data/themes/Light/sources/gamepad.svg rename to frontend/data/themes/Light/sources/gamepad.svg diff --git a/UI/data/themes/Light/sources/globe.svg b/frontend/data/themes/Light/sources/globe.svg similarity index 100% rename from UI/data/themes/Light/sources/globe.svg rename to frontend/data/themes/Light/sources/globe.svg diff --git a/UI/data/themes/Light/sources/group.svg b/frontend/data/themes/Light/sources/group.svg similarity index 100% rename from UI/data/themes/Light/sources/group.svg rename to frontend/data/themes/Light/sources/group.svg diff --git a/UI/data/themes/Light/sources/image.svg b/frontend/data/themes/Light/sources/image.svg similarity index 100% rename from UI/data/themes/Light/sources/image.svg rename to frontend/data/themes/Light/sources/image.svg diff --git a/UI/data/themes/Light/sources/media.svg b/frontend/data/themes/Light/sources/media.svg similarity index 100% rename from UI/data/themes/Light/sources/media.svg rename to frontend/data/themes/Light/sources/media.svg diff --git a/UI/data/themes/Light/sources/microphone.svg b/frontend/data/themes/Light/sources/microphone.svg similarity index 100% rename from UI/data/themes/Light/sources/microphone.svg rename to frontend/data/themes/Light/sources/microphone.svg diff --git a/UI/data/themes/Light/sources/scene.svg b/frontend/data/themes/Light/sources/scene.svg similarity index 100% rename from UI/data/themes/Light/sources/scene.svg rename to frontend/data/themes/Light/sources/scene.svg diff --git a/UI/data/themes/Light/sources/slideshow.svg b/frontend/data/themes/Light/sources/slideshow.svg similarity index 100% rename from UI/data/themes/Light/sources/slideshow.svg rename to frontend/data/themes/Light/sources/slideshow.svg diff --git a/UI/data/themes/Light/sources/text.svg b/frontend/data/themes/Light/sources/text.svg similarity index 100% rename from UI/data/themes/Light/sources/text.svg rename to frontend/data/themes/Light/sources/text.svg diff --git a/UI/data/themes/Light/sources/window.svg b/frontend/data/themes/Light/sources/window.svg similarity index 100% rename from UI/data/themes/Light/sources/window.svg rename to frontend/data/themes/Light/sources/window.svg diff --git a/UI/data/themes/Light/sources/windowaudio.svg b/frontend/data/themes/Light/sources/windowaudio.svg similarity index 100% rename from UI/data/themes/Light/sources/windowaudio.svg rename to frontend/data/themes/Light/sources/windowaudio.svg diff --git a/UI/data/themes/Light/trash.svg b/frontend/data/themes/Light/trash.svg similarity index 100% rename from UI/data/themes/Light/trash.svg rename to frontend/data/themes/Light/trash.svg diff --git a/UI/data/themes/Light/up.svg b/frontend/data/themes/Light/up.svg similarity index 100% rename from UI/data/themes/Light/up.svg rename to frontend/data/themes/Light/up.svg diff --git a/UI/data/themes/Light/updown.svg b/frontend/data/themes/Light/updown.svg similarity index 100% rename from UI/data/themes/Light/updown.svg rename to frontend/data/themes/Light/updown.svg diff --git a/UI/data/themes/Light/visible.svg b/frontend/data/themes/Light/visible.svg similarity index 100% rename from UI/data/themes/Light/visible.svg rename to frontend/data/themes/Light/visible.svg diff --git a/UI/data/themes/Rachni/checkbox_checked.png b/frontend/data/themes/Rachni/checkbox_checked.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked.png rename to frontend/data/themes/Rachni/checkbox_checked.png diff --git a/UI/data/themes/Rachni/checkbox_checked_disabled.png b/frontend/data/themes/Rachni/checkbox_checked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked_disabled.png rename to frontend/data/themes/Rachni/checkbox_checked_disabled.png diff --git a/UI/data/themes/Rachni/checkbox_checked_focus.png b/frontend/data/themes/Rachni/checkbox_checked_focus.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked_focus.png rename to frontend/data/themes/Rachni/checkbox_checked_focus.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked.png b/frontend/data/themes/Rachni/checkbox_unchecked.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked.png rename to frontend/data/themes/Rachni/checkbox_unchecked.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked_disabled.png b/frontend/data/themes/Rachni/checkbox_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked_disabled.png rename to frontend/data/themes/Rachni/checkbox_unchecked_disabled.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked_focus.png b/frontend/data/themes/Rachni/checkbox_unchecked_focus.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked_focus.png rename to frontend/data/themes/Rachni/checkbox_unchecked_focus.png diff --git a/UI/data/themes/Rachni/down_arrow.png b/frontend/data/themes/Rachni/down_arrow.png similarity index 100% rename from UI/data/themes/Rachni/down_arrow.png rename to frontend/data/themes/Rachni/down_arrow.png diff --git a/UI/data/themes/Rachni/down_arrow_disabled.png b/frontend/data/themes/Rachni/down_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/down_arrow_disabled.png rename to frontend/data/themes/Rachni/down_arrow_disabled.png diff --git a/UI/data/themes/Rachni/left_arrow.png b/frontend/data/themes/Rachni/left_arrow.png similarity index 100% rename from UI/data/themes/Rachni/left_arrow.png rename to frontend/data/themes/Rachni/left_arrow.png diff --git a/UI/data/themes/Rachni/left_arrow_disabled.png b/frontend/data/themes/Rachni/left_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/left_arrow_disabled.png rename to frontend/data/themes/Rachni/left_arrow_disabled.png diff --git a/UI/data/themes/Rachni/radio_checked.png b/frontend/data/themes/Rachni/radio_checked.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked.png rename to frontend/data/themes/Rachni/radio_checked.png diff --git a/UI/data/themes/Rachni/radio_checked_disabled.png b/frontend/data/themes/Rachni/radio_checked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked_disabled.png rename to frontend/data/themes/Rachni/radio_checked_disabled.png diff --git a/UI/data/themes/Rachni/radio_checked_focus.png b/frontend/data/themes/Rachni/radio_checked_focus.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked_focus.png rename to frontend/data/themes/Rachni/radio_checked_focus.png diff --git a/UI/data/themes/Rachni/radio_unchecked.png b/frontend/data/themes/Rachni/radio_unchecked.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked.png rename to frontend/data/themes/Rachni/radio_unchecked.png diff --git a/UI/data/themes/Rachni/radio_unchecked_disabled.png b/frontend/data/themes/Rachni/radio_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked_disabled.png rename to frontend/data/themes/Rachni/radio_unchecked_disabled.png diff --git a/UI/data/themes/Rachni/radio_unchecked_focus.png b/frontend/data/themes/Rachni/radio_unchecked_focus.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked_focus.png rename to frontend/data/themes/Rachni/radio_unchecked_focus.png diff --git a/UI/data/themes/Rachni/right_arrow.png b/frontend/data/themes/Rachni/right_arrow.png similarity index 100% rename from UI/data/themes/Rachni/right_arrow.png rename to frontend/data/themes/Rachni/right_arrow.png diff --git a/UI/data/themes/Rachni/right_arrow_disabled.png b/frontend/data/themes/Rachni/right_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/right_arrow_disabled.png rename to frontend/data/themes/Rachni/right_arrow_disabled.png diff --git a/UI/data/themes/Rachni/sizegrip.png b/frontend/data/themes/Rachni/sizegrip.png similarity index 100% rename from UI/data/themes/Rachni/sizegrip.png rename to frontend/data/themes/Rachni/sizegrip.png diff --git a/UI/data/themes/Rachni/up_arrow.png b/frontend/data/themes/Rachni/up_arrow.png similarity index 100% rename from UI/data/themes/Rachni/up_arrow.png rename to frontend/data/themes/Rachni/up_arrow.png diff --git a/UI/data/themes/Rachni/up_arrow_disabled.png b/frontend/data/themes/Rachni/up_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/up_arrow_disabled.png rename to frontend/data/themes/Rachni/up_arrow_disabled.png diff --git a/UI/data/themes/System.obt b/frontend/data/themes/System.obt similarity index 100% rename from UI/data/themes/System.obt rename to frontend/data/themes/System.obt diff --git a/UI/data/themes/Yami.obt b/frontend/data/themes/Yami.obt similarity index 100% rename from UI/data/themes/Yami.obt rename to frontend/data/themes/Yami.obt diff --git a/UI/data/themes/Yami/checkbox_checked.svg b/frontend/data/themes/Yami/checkbox_checked.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked.svg rename to frontend/data/themes/Yami/checkbox_checked.svg diff --git a/UI/data/themes/Yami/checkbox_checked_disabled.svg b/frontend/data/themes/Yami/checkbox_checked_disabled.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked_disabled.svg rename to frontend/data/themes/Yami/checkbox_checked_disabled.svg diff --git a/UI/data/themes/Yami/checkbox_checked_focus.svg b/frontend/data/themes/Yami/checkbox_checked_focus.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked_focus.svg rename to frontend/data/themes/Yami/checkbox_checked_focus.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked.svg b/frontend/data/themes/Yami/checkbox_unchecked.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked.svg rename to frontend/data/themes/Yami/checkbox_unchecked.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked_disabled.svg b/frontend/data/themes/Yami/checkbox_unchecked_disabled.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked_disabled.svg rename to frontend/data/themes/Yami/checkbox_unchecked_disabled.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked_focus.svg b/frontend/data/themes/Yami/checkbox_unchecked_focus.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked_focus.svg rename to frontend/data/themes/Yami/checkbox_unchecked_focus.svg diff --git a/UI/data/themes/Yami_Acri.ovt b/frontend/data/themes/Yami_Acri.ovt similarity index 100% rename from UI/data/themes/Yami_Acri.ovt rename to frontend/data/themes/Yami_Acri.ovt diff --git a/UI/data/themes/Yami_Classic.ovt b/frontend/data/themes/Yami_Classic.ovt similarity index 100% rename from UI/data/themes/Yami_Classic.ovt rename to frontend/data/themes/Yami_Classic.ovt diff --git a/UI/data/themes/Yami_Default.ovt b/frontend/data/themes/Yami_Default.ovt similarity index 100% rename from UI/data/themes/Yami_Default.ovt rename to frontend/data/themes/Yami_Default.ovt diff --git a/UI/data/themes/Yami_Grey.ovt b/frontend/data/themes/Yami_Grey.ovt similarity index 100% rename from UI/data/themes/Yami_Grey.ovt rename to frontend/data/themes/Yami_Grey.ovt diff --git a/UI/data/themes/Yami_Light.ovt b/frontend/data/themes/Yami_Light.ovt similarity index 100% rename from UI/data/themes/Yami_Light.ovt rename to frontend/data/themes/Yami_Light.ovt diff --git a/UI/data/themes/Yami_Rachni.ovt b/frontend/data/themes/Yami_Rachni.ovt similarity index 100% rename from UI/data/themes/Yami_Rachni.ovt rename to frontend/data/themes/Yami_Rachni.ovt From e6430ab1d84f9232191928d27b2f6599196c8c0f Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:19:27 +0100 Subject: [PATCH 32/37] frontend: Migrate Qt UI files --- {UI => frontend}/forms/AutoConfigFinishPage.ui | 0 {UI => frontend}/forms/AutoConfigStartPage.ui | 0 {UI => frontend}/forms/AutoConfigStreamPage.ui | 2 +- {UI => frontend}/forms/AutoConfigTestPage.ui | 0 {UI => frontend}/forms/AutoConfigVideoPage.ui | 0 {UI => frontend}/forms/ColorSelect.ui | 0 {UI => frontend}/forms/OBSAbout.ui | 2 +- {UI => frontend}/forms/OBSAdvAudio.ui | 0 {UI => frontend}/forms/OBSBasic.ui | 16 ++++++++-------- {UI => frontend}/forms/OBSBasicControls.ui | 2 +- {UI => frontend}/forms/OBSBasicFilters.ui | 4 ++-- {UI => frontend}/forms/OBSBasicInteraction.ui | 2 +- {UI => frontend}/forms/OBSBasicProperties.ui | 2 +- {UI => frontend}/forms/OBSBasicSettings.ui | 4 ++-- {UI => frontend}/forms/OBSBasicSourceSelect.ui | 0 {UI => frontend}/forms/OBSBasicTransform.ui | 0 {UI => frontend}/forms/OBSBasicVCamConfig.ui | 0 {UI => frontend}/forms/OBSExtraBrowsers.ui | 0 {UI => frontend}/forms/OBSImporter.ui | 0 {UI => frontend}/forms/OBSLogReply.ui | 0 {UI => frontend}/forms/OBSLogViewer.ui | 0 {UI => frontend}/forms/OBSMissingFiles.ui | 0 {UI => frontend}/forms/OBSPermissions.ui | 0 {UI => frontend}/forms/OBSRemux.ui | 0 {UI => frontend}/forms/OBSUpdate.ui | 0 {UI => frontend}/forms/OBSYoutubeActions.ui | 2 +- {UI => frontend}/forms/StatusBarWidget.ui | 0 {UI => frontend}/forms/XML-Schema-Qt5.15.xsd | 0 {UI => frontend}/forms/fonts/OpenSans-Bold.ttf | Bin .../forms/fonts/OpenSans-Italic.ttf | Bin .../forms/fonts/OpenSans-Regular.ttf | Bin {UI => frontend}/forms/images/active.png | Bin {UI => frontend}/forms/images/active_mac.png | Bin {UI => frontend}/forms/images/alert.svg | 0 {UI => frontend}/forms/images/cogs.svg | 0 {UI => frontend}/forms/images/collapse.svg | 0 {UI => frontend}/forms/images/dots-vert.svg | 0 {UI => frontend}/forms/images/dots.svg | 0 {UI => frontend}/forms/images/down.svg | 0 {UI => frontend}/forms/images/entry-clear.svg | 0 {UI => frontend}/forms/images/expand.svg | 0 {UI => frontend}/forms/images/filter.svg | 0 {UI => frontend}/forms/images/help.svg | 0 {UI => frontend}/forms/images/help_light.svg | 0 {UI => frontend}/forms/images/interact.svg | 0 {UI => frontend}/forms/images/invisible.svg | 0 {UI => frontend}/forms/images/locked.svg | 0 {UI => frontend}/forms/images/media-pause.svg | 0 .../forms/images/media/media_next.svg | 0 .../forms/images/media/media_pause.svg | 0 .../forms/images/media/media_play.svg | 0 .../forms/images/media/media_previous.svg | 0 .../forms/images/media/media_restart.svg | 0 .../forms/images/media/media_stop.svg | 0 {UI => frontend}/forms/images/minus.svg | 0 {UI => frontend}/forms/images/mute.svg | 0 {UI => frontend}/forms/images/network-bad.svg | 0 .../forms/images/network-disconnected.svg | 0 .../forms/images/network-excellent.svg | 0 {UI => frontend}/forms/images/network-good.svg | 0 .../forms/images/network-inactive.svg | 0 .../forms/images/network-mediocre.svg | 0 {UI => frontend}/forms/images/no_sources.svg | 0 {UI => frontend}/forms/images/obs.png | Bin {UI => frontend}/forms/images/obs_256x256.png | Bin {UI => frontend}/forms/images/obs_macos.png | Bin {UI => frontend}/forms/images/obs_macos.svg | 0 {UI => frontend}/forms/images/obs_paused.png | Bin .../forms/images/obs_paused_macos.png | Bin .../forms/images/obs_paused_macos.svg | 0 {UI => frontend}/forms/images/paused.png | Bin {UI => frontend}/forms/images/paused_mac.png | Bin {UI => frontend}/forms/images/plus.svg | 0 .../forms/images/recording-active.svg | 0 .../forms/images/recording-inactive.svg | 0 .../forms/images/recording-pause-inactive.svg | 0 .../forms/images/recording-pause.svg | 0 {UI => frontend}/forms/images/refresh.svg | 0 {UI => frontend}/forms/images/revert.svg | 0 {UI => frontend}/forms/images/right.svg | 0 {UI => frontend}/forms/images/save.svg | 0 .../forms/images/settings/accessibility.svg | 0 .../forms/images/settings/advanced.svg | 0 .../forms/images/settings/appearance.svg | 0 .../forms/images/settings/audio.svg | 0 .../forms/images/settings/general.svg | 0 .../forms/images/settings/hotkeys.svg | 0 .../forms/images/settings/output.svg | 0 .../forms/images/settings/stream.svg | 0 .../forms/images/settings/video.svg | 0 {UI => frontend}/forms/images/sources/brush.svg | 0 .../forms/images/sources/camera.svg | 0 .../forms/images/sources/default.svg | 0 .../forms/images/sources/gamepad.svg | 0 {UI => frontend}/forms/images/sources/globe.svg | 0 {UI => frontend}/forms/images/sources/group.svg | 0 {UI => frontend}/forms/images/sources/image.svg | 0 {UI => frontend}/forms/images/sources/media.svg | 0 .../forms/images/sources/microphone.svg | 0 {UI => frontend}/forms/images/sources/scene.svg | 0 .../forms/images/sources/slideshow.svg | 0 {UI => frontend}/forms/images/sources/text.svg | 0 .../forms/images/sources/window.svg | 0 .../forms/images/sources/windowaudio.svg | 0 .../forms/images/streaming-active.svg | 0 .../forms/images/streaming-inactive.svg | 0 {UI => frontend}/forms/images/trash.svg | 0 {UI => frontend}/forms/images/tray_active.png | Bin .../forms/images/tray_active_macos.png | Bin .../forms/images/tray_active_macos.svg | 0 {UI => frontend}/forms/images/unassigned.svg | 0 {UI => frontend}/forms/images/unlocked.svg | 0 {UI => frontend}/forms/images/up.svg | 0 {UI => frontend}/forms/images/visible.svg | 0 {UI => frontend}/forms/images/warning.svg | 0 {UI => frontend}/forms/obs.qrc | 0 .../source-toolbar/browser-source-toolbar.ui | 0 .../source-toolbar/color-source-toolbar.ui | 0 .../source-toolbar/device-select-toolbar.ui | 0 .../source-toolbar/game-capture-toolbar.ui | 0 .../source-toolbar/image-source-toolbar.ui | 0 .../forms/source-toolbar/media-controls.ui | 4 ++-- .../forms/source-toolbar/text-source-toolbar.ui | 0 123 files changed, 20 insertions(+), 20 deletions(-) rename {UI => frontend}/forms/AutoConfigFinishPage.ui (100%) rename {UI => frontend}/forms/AutoConfigStartPage.ui (100%) rename {UI => frontend}/forms/AutoConfigStreamPage.ui (99%) rename {UI => frontend}/forms/AutoConfigTestPage.ui (100%) rename {UI => frontend}/forms/AutoConfigVideoPage.ui (100%) rename {UI => frontend}/forms/ColorSelect.ui (100%) rename {UI => frontend}/forms/OBSAbout.ui (99%) rename {UI => frontend}/forms/OBSAdvAudio.ui (100%) rename {UI => frontend}/forms/OBSBasic.ui (99%) rename {UI => frontend}/forms/OBSBasicControls.ui (99%) rename {UI => frontend}/forms/OBSBasicFilters.ui (99%) rename {UI => frontend}/forms/OBSBasicInteraction.ui (96%) rename {UI => frontend}/forms/OBSBasicProperties.ui (98%) rename {UI => frontend}/forms/OBSBasicSettings.ui (99%) rename {UI => frontend}/forms/OBSBasicSourceSelect.ui (100%) rename {UI => frontend}/forms/OBSBasicTransform.ui (100%) rename {UI => frontend}/forms/OBSBasicVCamConfig.ui (100%) rename {UI => frontend}/forms/OBSExtraBrowsers.ui (100%) rename {UI => frontend}/forms/OBSImporter.ui (100%) rename {UI => frontend}/forms/OBSLogReply.ui (100%) rename {UI => frontend}/forms/OBSLogViewer.ui (100%) rename {UI => frontend}/forms/OBSMissingFiles.ui (100%) rename {UI => frontend}/forms/OBSPermissions.ui (100%) rename {UI => frontend}/forms/OBSRemux.ui (100%) rename {UI => frontend}/forms/OBSUpdate.ui (100%) rename {UI => frontend}/forms/OBSYoutubeActions.ui (99%) rename {UI => frontend}/forms/StatusBarWidget.ui (100%) rename {UI => frontend}/forms/XML-Schema-Qt5.15.xsd (100%) rename {UI => frontend}/forms/fonts/OpenSans-Bold.ttf (100%) rename {UI => frontend}/forms/fonts/OpenSans-Italic.ttf (100%) rename {UI => frontend}/forms/fonts/OpenSans-Regular.ttf (100%) rename {UI => frontend}/forms/images/active.png (100%) rename {UI => frontend}/forms/images/active_mac.png (100%) rename {UI => frontend}/forms/images/alert.svg (100%) rename {UI => frontend}/forms/images/cogs.svg (100%) rename {UI => frontend}/forms/images/collapse.svg (100%) rename {UI => frontend}/forms/images/dots-vert.svg (100%) rename {UI => frontend}/forms/images/dots.svg (100%) rename {UI => frontend}/forms/images/down.svg (100%) rename {UI => frontend}/forms/images/entry-clear.svg (100%) rename {UI => frontend}/forms/images/expand.svg (100%) rename {UI => frontend}/forms/images/filter.svg (100%) rename {UI => frontend}/forms/images/help.svg (100%) rename {UI => frontend}/forms/images/help_light.svg (100%) rename {UI => frontend}/forms/images/interact.svg (100%) rename {UI => frontend}/forms/images/invisible.svg (100%) rename {UI => frontend}/forms/images/locked.svg (100%) rename {UI => frontend}/forms/images/media-pause.svg (100%) rename {UI => frontend}/forms/images/media/media_next.svg (100%) rename {UI => frontend}/forms/images/media/media_pause.svg (100%) rename {UI => frontend}/forms/images/media/media_play.svg (100%) rename {UI => frontend}/forms/images/media/media_previous.svg (100%) rename {UI => frontend}/forms/images/media/media_restart.svg (100%) rename {UI => frontend}/forms/images/media/media_stop.svg (100%) rename {UI => frontend}/forms/images/minus.svg (100%) rename {UI => frontend}/forms/images/mute.svg (100%) rename {UI => frontend}/forms/images/network-bad.svg (100%) rename {UI => frontend}/forms/images/network-disconnected.svg (100%) rename {UI => frontend}/forms/images/network-excellent.svg (100%) rename {UI => frontend}/forms/images/network-good.svg (100%) rename {UI => frontend}/forms/images/network-inactive.svg (100%) rename {UI => frontend}/forms/images/network-mediocre.svg (100%) rename {UI => frontend}/forms/images/no_sources.svg (100%) rename {UI => frontend}/forms/images/obs.png (100%) rename {UI => frontend}/forms/images/obs_256x256.png (100%) rename {UI => frontend}/forms/images/obs_macos.png (100%) rename {UI => frontend}/forms/images/obs_macos.svg (100%) rename {UI => frontend}/forms/images/obs_paused.png (100%) rename {UI => frontend}/forms/images/obs_paused_macos.png (100%) rename {UI => frontend}/forms/images/obs_paused_macos.svg (100%) rename {UI => frontend}/forms/images/paused.png (100%) rename {UI => frontend}/forms/images/paused_mac.png (100%) rename {UI => frontend}/forms/images/plus.svg (100%) rename {UI => frontend}/forms/images/recording-active.svg (100%) rename {UI => frontend}/forms/images/recording-inactive.svg (100%) rename {UI => frontend}/forms/images/recording-pause-inactive.svg (100%) rename {UI => frontend}/forms/images/recording-pause.svg (100%) rename {UI => frontend}/forms/images/refresh.svg (100%) rename {UI => frontend}/forms/images/revert.svg (100%) rename {UI => frontend}/forms/images/right.svg (100%) rename {UI => frontend}/forms/images/save.svg (100%) rename {UI => frontend}/forms/images/settings/accessibility.svg (100%) rename {UI => frontend}/forms/images/settings/advanced.svg (100%) rename {UI => frontend}/forms/images/settings/appearance.svg (100%) rename {UI => frontend}/forms/images/settings/audio.svg (100%) rename {UI => frontend}/forms/images/settings/general.svg (100%) rename {UI => frontend}/forms/images/settings/hotkeys.svg (100%) rename {UI => frontend}/forms/images/settings/output.svg (100%) rename {UI => frontend}/forms/images/settings/stream.svg (100%) rename {UI => frontend}/forms/images/settings/video.svg (100%) rename {UI => frontend}/forms/images/sources/brush.svg (100%) rename {UI => frontend}/forms/images/sources/camera.svg (100%) rename {UI => frontend}/forms/images/sources/default.svg (100%) rename {UI => frontend}/forms/images/sources/gamepad.svg (100%) rename {UI => frontend}/forms/images/sources/globe.svg (100%) rename {UI => frontend}/forms/images/sources/group.svg (100%) rename {UI => frontend}/forms/images/sources/image.svg (100%) rename {UI => frontend}/forms/images/sources/media.svg (100%) rename {UI => frontend}/forms/images/sources/microphone.svg (100%) rename {UI => frontend}/forms/images/sources/scene.svg (100%) rename {UI => frontend}/forms/images/sources/slideshow.svg (100%) rename {UI => frontend}/forms/images/sources/text.svg (100%) rename {UI => frontend}/forms/images/sources/window.svg (100%) rename {UI => frontend}/forms/images/sources/windowaudio.svg (100%) rename {UI => frontend}/forms/images/streaming-active.svg (100%) rename {UI => frontend}/forms/images/streaming-inactive.svg (100%) rename {UI => frontend}/forms/images/trash.svg (100%) rename {UI => frontend}/forms/images/tray_active.png (100%) rename {UI => frontend}/forms/images/tray_active_macos.png (100%) rename {UI => frontend}/forms/images/tray_active_macos.svg (100%) rename {UI => frontend}/forms/images/unassigned.svg (100%) rename {UI => frontend}/forms/images/unlocked.svg (100%) rename {UI => frontend}/forms/images/up.svg (100%) rename {UI => frontend}/forms/images/visible.svg (100%) rename {UI => frontend}/forms/images/warning.svg (100%) rename {UI => frontend}/forms/obs.qrc (100%) rename {UI => frontend}/forms/source-toolbar/browser-source-toolbar.ui (100%) rename {UI => frontend}/forms/source-toolbar/color-source-toolbar.ui (100%) rename {UI => frontend}/forms/source-toolbar/device-select-toolbar.ui (100%) rename {UI => frontend}/forms/source-toolbar/game-capture-toolbar.ui (100%) rename {UI => frontend}/forms/source-toolbar/image-source-toolbar.ui (100%) rename {UI => frontend}/forms/source-toolbar/media-controls.ui (98%) rename {UI => frontend}/forms/source-toolbar/text-source-toolbar.ui (100%) diff --git a/UI/forms/AutoConfigFinishPage.ui b/frontend/forms/AutoConfigFinishPage.ui similarity index 100% rename from UI/forms/AutoConfigFinishPage.ui rename to frontend/forms/AutoConfigFinishPage.ui diff --git a/UI/forms/AutoConfigStartPage.ui b/frontend/forms/AutoConfigStartPage.ui similarity index 100% rename from UI/forms/AutoConfigStartPage.ui rename to frontend/forms/AutoConfigStartPage.ui diff --git a/UI/forms/AutoConfigStreamPage.ui b/frontend/forms/AutoConfigStreamPage.ui similarity index 99% rename from UI/forms/AutoConfigStreamPage.ui rename to frontend/forms/AutoConfigStreamPage.ui index 241292c6d..18c219ceb 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/frontend/forms/AutoConfigStreamPage.ui @@ -534,7 +534,7 @@ UrlPushButton QPushButton -
url-push-button.hpp
+
components/UrlPushButton.hpp
diff --git a/UI/forms/AutoConfigTestPage.ui b/frontend/forms/AutoConfigTestPage.ui similarity index 100% rename from UI/forms/AutoConfigTestPage.ui rename to frontend/forms/AutoConfigTestPage.ui diff --git a/UI/forms/AutoConfigVideoPage.ui b/frontend/forms/AutoConfigVideoPage.ui similarity index 100% rename from UI/forms/AutoConfigVideoPage.ui rename to frontend/forms/AutoConfigVideoPage.ui diff --git a/UI/forms/ColorSelect.ui b/frontend/forms/ColorSelect.ui similarity index 100% rename from UI/forms/ColorSelect.ui rename to frontend/forms/ColorSelect.ui diff --git a/UI/forms/OBSAbout.ui b/frontend/forms/OBSAbout.ui similarity index 99% rename from UI/forms/OBSAbout.ui rename to frontend/forms/OBSAbout.ui index 37ad2a278..415dc3f9e 100644 --- a/UI/forms/OBSAbout.ui +++ b/frontend/forms/OBSAbout.ui @@ -243,7 +243,7 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
diff --git a/UI/forms/OBSAdvAudio.ui b/frontend/forms/OBSAdvAudio.ui similarity index 100% rename from UI/forms/OBSAdvAudio.ui rename to frontend/forms/OBSAdvAudio.ui diff --git a/UI/forms/OBSBasic.ui b/frontend/forms/OBSBasic.ui similarity index 99% rename from UI/forms/OBSBasic.ui rename to frontend/forms/OBSBasic.ui index 53e457ad3..832038b09 100644 --- a/UI/forms/OBSBasic.ui +++ b/frontend/forms/OBSBasic.ui @@ -2218,18 +2218,18 @@ OBSBasicPreview QWidget -
window-basic-preview.hpp
+
widgets/OBSBasicPreview.hpp
1
OBSBasicStatusBar QStatusBar -
window-basic-status-bar.hpp
+
widgets/OBSBasicStatusBar.hpp
HScrollArea QScrollArea -
horizontal-scroll-area.hpp
+
components/HScrollArea.hpp
1
@@ -2241,28 +2241,28 @@ SourceTree QListView -
source-tree.hpp
+
components/SourceTree.hpp
SceneTree QListWidget -
scene-tree.hpp
+
components/SceneTree.hpp
OBSDock QDockWidget -
window-dock.hpp
+
docks/OBSDock.hpp
1
OBSPreviewScalingLabel QLabel -
preview-controls.hpp
+
components/OBSPreviewScalingLabel.hpp
OBSPreviewScalingComboBox QComboBox -
preview-controls.hpp
+
components/OBSPreviewScalingComboBox.hpp
diff --git a/UI/forms/OBSBasicControls.ui b/frontend/forms/OBSBasicControls.ui similarity index 99% rename from UI/forms/OBSBasicControls.ui rename to frontend/forms/OBSBasicControls.ui index ce1c929d5..51bbd0175 100644 --- a/UI/forms/OBSBasicControls.ui +++ b/frontend/forms/OBSBasicControls.ui @@ -393,7 +393,7 @@ NonCheckableButton QPushButton -
noncheckable-button.hpp
+
components/NonCheckableButton.hpp
diff --git a/UI/forms/OBSBasicFilters.ui b/frontend/forms/OBSBasicFilters.ui similarity index 99% rename from UI/forms/OBSBasicFilters.ui rename to frontend/forms/OBSBasicFilters.ui index ec964e268..c9cefaefc 100644 --- a/UI/forms/OBSBasicFilters.ui +++ b/frontend/forms/OBSBasicFilters.ui @@ -653,13 +653,13 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
FocusList QListWidget -
focus-list.hpp
+
components/FocusList.hpp
diff --git a/UI/forms/OBSBasicInteraction.ui b/frontend/forms/OBSBasicInteraction.ui similarity index 96% rename from UI/forms/OBSBasicInteraction.ui rename to frontend/forms/OBSBasicInteraction.ui index 65d313bc9..0ed573f50 100644 --- a/UI/forms/OBSBasicInteraction.ui +++ b/frontend/forms/OBSBasicInteraction.ui @@ -39,7 +39,7 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
diff --git a/UI/forms/OBSBasicProperties.ui b/frontend/forms/OBSBasicProperties.ui similarity index 98% rename from UI/forms/OBSBasicProperties.ui rename to frontend/forms/OBSBasicProperties.ui index 26145f772..66a25488e 100644 --- a/UI/forms/OBSBasicProperties.ui +++ b/frontend/forms/OBSBasicProperties.ui @@ -121,7 +121,7 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
diff --git a/UI/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui similarity index 99% rename from UI/forms/OBSBasicSettings.ui rename to frontend/forms/OBSBasicSettings.ui index 267f4a3a4..50e69dd2e 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -8472,12 +8472,12 @@ UrlPushButton QPushButton -
url-push-button.hpp
+
components/UrlPushButton.hpp
OBSHotkeyEdit QLineEdit -
hotkey-edit.hpp
+
settings/OBSHotkeyEdit.hpp
diff --git a/UI/forms/OBSBasicSourceSelect.ui b/frontend/forms/OBSBasicSourceSelect.ui similarity index 100% rename from UI/forms/OBSBasicSourceSelect.ui rename to frontend/forms/OBSBasicSourceSelect.ui diff --git a/UI/forms/OBSBasicTransform.ui b/frontend/forms/OBSBasicTransform.ui similarity index 100% rename from UI/forms/OBSBasicTransform.ui rename to frontend/forms/OBSBasicTransform.ui diff --git a/UI/forms/OBSBasicVCamConfig.ui b/frontend/forms/OBSBasicVCamConfig.ui similarity index 100% rename from UI/forms/OBSBasicVCamConfig.ui rename to frontend/forms/OBSBasicVCamConfig.ui diff --git a/UI/forms/OBSExtraBrowsers.ui b/frontend/forms/OBSExtraBrowsers.ui similarity index 100% rename from UI/forms/OBSExtraBrowsers.ui rename to frontend/forms/OBSExtraBrowsers.ui diff --git a/UI/forms/OBSImporter.ui b/frontend/forms/OBSImporter.ui similarity index 100% rename from UI/forms/OBSImporter.ui rename to frontend/forms/OBSImporter.ui diff --git a/UI/forms/OBSLogReply.ui b/frontend/forms/OBSLogReply.ui similarity index 100% rename from UI/forms/OBSLogReply.ui rename to frontend/forms/OBSLogReply.ui diff --git a/UI/forms/OBSLogViewer.ui b/frontend/forms/OBSLogViewer.ui similarity index 100% rename from UI/forms/OBSLogViewer.ui rename to frontend/forms/OBSLogViewer.ui diff --git a/UI/forms/OBSMissingFiles.ui b/frontend/forms/OBSMissingFiles.ui similarity index 100% rename from UI/forms/OBSMissingFiles.ui rename to frontend/forms/OBSMissingFiles.ui diff --git a/UI/forms/OBSPermissions.ui b/frontend/forms/OBSPermissions.ui similarity index 100% rename from UI/forms/OBSPermissions.ui rename to frontend/forms/OBSPermissions.ui diff --git a/UI/forms/OBSRemux.ui b/frontend/forms/OBSRemux.ui similarity index 100% rename from UI/forms/OBSRemux.ui rename to frontend/forms/OBSRemux.ui diff --git a/UI/forms/OBSUpdate.ui b/frontend/forms/OBSUpdate.ui similarity index 100% rename from UI/forms/OBSUpdate.ui rename to frontend/forms/OBSUpdate.ui diff --git a/UI/forms/OBSYoutubeActions.ui b/frontend/forms/OBSYoutubeActions.ui similarity index 99% rename from UI/forms/OBSYoutubeActions.ui rename to frontend/forms/OBSYoutubeActions.ui index 3c7ddcc78..1f38008cd 100644 --- a/UI/forms/OBSYoutubeActions.ui +++ b/frontend/forms/OBSYoutubeActions.ui @@ -687,7 +687,7 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
diff --git a/UI/forms/StatusBarWidget.ui b/frontend/forms/StatusBarWidget.ui similarity index 100% rename from UI/forms/StatusBarWidget.ui rename to frontend/forms/StatusBarWidget.ui diff --git a/UI/forms/XML-Schema-Qt5.15.xsd b/frontend/forms/XML-Schema-Qt5.15.xsd similarity index 100% rename from UI/forms/XML-Schema-Qt5.15.xsd rename to frontend/forms/XML-Schema-Qt5.15.xsd diff --git a/UI/forms/fonts/OpenSans-Bold.ttf b/frontend/forms/fonts/OpenSans-Bold.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Bold.ttf rename to frontend/forms/fonts/OpenSans-Bold.ttf diff --git a/UI/forms/fonts/OpenSans-Italic.ttf b/frontend/forms/fonts/OpenSans-Italic.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Italic.ttf rename to frontend/forms/fonts/OpenSans-Italic.ttf diff --git a/UI/forms/fonts/OpenSans-Regular.ttf b/frontend/forms/fonts/OpenSans-Regular.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Regular.ttf rename to frontend/forms/fonts/OpenSans-Regular.ttf diff --git a/UI/forms/images/active.png b/frontend/forms/images/active.png similarity index 100% rename from UI/forms/images/active.png rename to frontend/forms/images/active.png diff --git a/UI/forms/images/active_mac.png b/frontend/forms/images/active_mac.png similarity index 100% rename from UI/forms/images/active_mac.png rename to frontend/forms/images/active_mac.png diff --git a/UI/forms/images/alert.svg b/frontend/forms/images/alert.svg similarity index 100% rename from UI/forms/images/alert.svg rename to frontend/forms/images/alert.svg diff --git a/UI/forms/images/cogs.svg b/frontend/forms/images/cogs.svg similarity index 100% rename from UI/forms/images/cogs.svg rename to frontend/forms/images/cogs.svg diff --git a/UI/forms/images/collapse.svg b/frontend/forms/images/collapse.svg similarity index 100% rename from UI/forms/images/collapse.svg rename to frontend/forms/images/collapse.svg diff --git a/UI/forms/images/dots-vert.svg b/frontend/forms/images/dots-vert.svg similarity index 100% rename from UI/forms/images/dots-vert.svg rename to frontend/forms/images/dots-vert.svg diff --git a/UI/forms/images/dots.svg b/frontend/forms/images/dots.svg similarity index 100% rename from UI/forms/images/dots.svg rename to frontend/forms/images/dots.svg diff --git a/UI/forms/images/down.svg b/frontend/forms/images/down.svg similarity index 100% rename from UI/forms/images/down.svg rename to frontend/forms/images/down.svg diff --git a/UI/forms/images/entry-clear.svg b/frontend/forms/images/entry-clear.svg similarity index 100% rename from UI/forms/images/entry-clear.svg rename to frontend/forms/images/entry-clear.svg diff --git a/UI/forms/images/expand.svg b/frontend/forms/images/expand.svg similarity index 100% rename from UI/forms/images/expand.svg rename to frontend/forms/images/expand.svg diff --git a/UI/forms/images/filter.svg b/frontend/forms/images/filter.svg similarity index 100% rename from UI/forms/images/filter.svg rename to frontend/forms/images/filter.svg diff --git a/UI/forms/images/help.svg b/frontend/forms/images/help.svg similarity index 100% rename from UI/forms/images/help.svg rename to frontend/forms/images/help.svg diff --git a/UI/forms/images/help_light.svg b/frontend/forms/images/help_light.svg similarity index 100% rename from UI/forms/images/help_light.svg rename to frontend/forms/images/help_light.svg diff --git a/UI/forms/images/interact.svg b/frontend/forms/images/interact.svg similarity index 100% rename from UI/forms/images/interact.svg rename to frontend/forms/images/interact.svg diff --git a/UI/forms/images/invisible.svg b/frontend/forms/images/invisible.svg similarity index 100% rename from UI/forms/images/invisible.svg rename to frontend/forms/images/invisible.svg diff --git a/UI/forms/images/locked.svg b/frontend/forms/images/locked.svg similarity index 100% rename from UI/forms/images/locked.svg rename to frontend/forms/images/locked.svg diff --git a/UI/forms/images/media-pause.svg b/frontend/forms/images/media-pause.svg similarity index 100% rename from UI/forms/images/media-pause.svg rename to frontend/forms/images/media-pause.svg diff --git a/UI/forms/images/media/media_next.svg b/frontend/forms/images/media/media_next.svg similarity index 100% rename from UI/forms/images/media/media_next.svg rename to frontend/forms/images/media/media_next.svg diff --git a/UI/forms/images/media/media_pause.svg b/frontend/forms/images/media/media_pause.svg similarity index 100% rename from UI/forms/images/media/media_pause.svg rename to frontend/forms/images/media/media_pause.svg diff --git a/UI/forms/images/media/media_play.svg b/frontend/forms/images/media/media_play.svg similarity index 100% rename from UI/forms/images/media/media_play.svg rename to frontend/forms/images/media/media_play.svg diff --git a/UI/forms/images/media/media_previous.svg b/frontend/forms/images/media/media_previous.svg similarity index 100% rename from UI/forms/images/media/media_previous.svg rename to frontend/forms/images/media/media_previous.svg diff --git a/UI/forms/images/media/media_restart.svg b/frontend/forms/images/media/media_restart.svg similarity index 100% rename from UI/forms/images/media/media_restart.svg rename to frontend/forms/images/media/media_restart.svg diff --git a/UI/forms/images/media/media_stop.svg b/frontend/forms/images/media/media_stop.svg similarity index 100% rename from UI/forms/images/media/media_stop.svg rename to frontend/forms/images/media/media_stop.svg diff --git a/UI/forms/images/minus.svg b/frontend/forms/images/minus.svg similarity index 100% rename from UI/forms/images/minus.svg rename to frontend/forms/images/minus.svg diff --git a/UI/forms/images/mute.svg b/frontend/forms/images/mute.svg similarity index 100% rename from UI/forms/images/mute.svg rename to frontend/forms/images/mute.svg diff --git a/UI/forms/images/network-bad.svg b/frontend/forms/images/network-bad.svg similarity index 100% rename from UI/forms/images/network-bad.svg rename to frontend/forms/images/network-bad.svg diff --git a/UI/forms/images/network-disconnected.svg b/frontend/forms/images/network-disconnected.svg similarity index 100% rename from UI/forms/images/network-disconnected.svg rename to frontend/forms/images/network-disconnected.svg diff --git a/UI/forms/images/network-excellent.svg b/frontend/forms/images/network-excellent.svg similarity index 100% rename from UI/forms/images/network-excellent.svg rename to frontend/forms/images/network-excellent.svg diff --git a/UI/forms/images/network-good.svg b/frontend/forms/images/network-good.svg similarity index 100% rename from UI/forms/images/network-good.svg rename to frontend/forms/images/network-good.svg diff --git a/UI/forms/images/network-inactive.svg b/frontend/forms/images/network-inactive.svg similarity index 100% rename from UI/forms/images/network-inactive.svg rename to frontend/forms/images/network-inactive.svg diff --git a/UI/forms/images/network-mediocre.svg b/frontend/forms/images/network-mediocre.svg similarity index 100% rename from UI/forms/images/network-mediocre.svg rename to frontend/forms/images/network-mediocre.svg diff --git a/UI/forms/images/no_sources.svg b/frontend/forms/images/no_sources.svg similarity index 100% rename from UI/forms/images/no_sources.svg rename to frontend/forms/images/no_sources.svg diff --git a/UI/forms/images/obs.png b/frontend/forms/images/obs.png similarity index 100% rename from UI/forms/images/obs.png rename to frontend/forms/images/obs.png diff --git a/UI/forms/images/obs_256x256.png b/frontend/forms/images/obs_256x256.png similarity index 100% rename from UI/forms/images/obs_256x256.png rename to frontend/forms/images/obs_256x256.png diff --git a/UI/forms/images/obs_macos.png b/frontend/forms/images/obs_macos.png similarity index 100% rename from UI/forms/images/obs_macos.png rename to frontend/forms/images/obs_macos.png diff --git a/UI/forms/images/obs_macos.svg b/frontend/forms/images/obs_macos.svg similarity index 100% rename from UI/forms/images/obs_macos.svg rename to frontend/forms/images/obs_macos.svg diff --git a/UI/forms/images/obs_paused.png b/frontend/forms/images/obs_paused.png similarity index 100% rename from UI/forms/images/obs_paused.png rename to frontend/forms/images/obs_paused.png diff --git a/UI/forms/images/obs_paused_macos.png b/frontend/forms/images/obs_paused_macos.png similarity index 100% rename from UI/forms/images/obs_paused_macos.png rename to frontend/forms/images/obs_paused_macos.png diff --git a/UI/forms/images/obs_paused_macos.svg b/frontend/forms/images/obs_paused_macos.svg similarity index 100% rename from UI/forms/images/obs_paused_macos.svg rename to frontend/forms/images/obs_paused_macos.svg diff --git a/UI/forms/images/paused.png b/frontend/forms/images/paused.png similarity index 100% rename from UI/forms/images/paused.png rename to frontend/forms/images/paused.png diff --git a/UI/forms/images/paused_mac.png b/frontend/forms/images/paused_mac.png similarity index 100% rename from UI/forms/images/paused_mac.png rename to frontend/forms/images/paused_mac.png diff --git a/UI/forms/images/plus.svg b/frontend/forms/images/plus.svg similarity index 100% rename from UI/forms/images/plus.svg rename to frontend/forms/images/plus.svg diff --git a/UI/forms/images/recording-active.svg b/frontend/forms/images/recording-active.svg similarity index 100% rename from UI/forms/images/recording-active.svg rename to frontend/forms/images/recording-active.svg diff --git a/UI/forms/images/recording-inactive.svg b/frontend/forms/images/recording-inactive.svg similarity index 100% rename from UI/forms/images/recording-inactive.svg rename to frontend/forms/images/recording-inactive.svg diff --git a/UI/forms/images/recording-pause-inactive.svg b/frontend/forms/images/recording-pause-inactive.svg similarity index 100% rename from UI/forms/images/recording-pause-inactive.svg rename to frontend/forms/images/recording-pause-inactive.svg diff --git a/UI/forms/images/recording-pause.svg b/frontend/forms/images/recording-pause.svg similarity index 100% rename from UI/forms/images/recording-pause.svg rename to frontend/forms/images/recording-pause.svg diff --git a/UI/forms/images/refresh.svg b/frontend/forms/images/refresh.svg similarity index 100% rename from UI/forms/images/refresh.svg rename to frontend/forms/images/refresh.svg diff --git a/UI/forms/images/revert.svg b/frontend/forms/images/revert.svg similarity index 100% rename from UI/forms/images/revert.svg rename to frontend/forms/images/revert.svg diff --git a/UI/forms/images/right.svg b/frontend/forms/images/right.svg similarity index 100% rename from UI/forms/images/right.svg rename to frontend/forms/images/right.svg diff --git a/UI/forms/images/save.svg b/frontend/forms/images/save.svg similarity index 100% rename from UI/forms/images/save.svg rename to frontend/forms/images/save.svg diff --git a/UI/forms/images/settings/accessibility.svg b/frontend/forms/images/settings/accessibility.svg similarity index 100% rename from UI/forms/images/settings/accessibility.svg rename to frontend/forms/images/settings/accessibility.svg diff --git a/UI/forms/images/settings/advanced.svg b/frontend/forms/images/settings/advanced.svg similarity index 100% rename from UI/forms/images/settings/advanced.svg rename to frontend/forms/images/settings/advanced.svg diff --git a/UI/forms/images/settings/appearance.svg b/frontend/forms/images/settings/appearance.svg similarity index 100% rename from UI/forms/images/settings/appearance.svg rename to frontend/forms/images/settings/appearance.svg diff --git a/UI/forms/images/settings/audio.svg b/frontend/forms/images/settings/audio.svg similarity index 100% rename from UI/forms/images/settings/audio.svg rename to frontend/forms/images/settings/audio.svg diff --git a/UI/forms/images/settings/general.svg b/frontend/forms/images/settings/general.svg similarity index 100% rename from UI/forms/images/settings/general.svg rename to frontend/forms/images/settings/general.svg diff --git a/UI/forms/images/settings/hotkeys.svg b/frontend/forms/images/settings/hotkeys.svg similarity index 100% rename from UI/forms/images/settings/hotkeys.svg rename to frontend/forms/images/settings/hotkeys.svg diff --git a/UI/forms/images/settings/output.svg b/frontend/forms/images/settings/output.svg similarity index 100% rename from UI/forms/images/settings/output.svg rename to frontend/forms/images/settings/output.svg diff --git a/UI/forms/images/settings/stream.svg b/frontend/forms/images/settings/stream.svg similarity index 100% rename from UI/forms/images/settings/stream.svg rename to frontend/forms/images/settings/stream.svg diff --git a/UI/forms/images/settings/video.svg b/frontend/forms/images/settings/video.svg similarity index 100% rename from UI/forms/images/settings/video.svg rename to frontend/forms/images/settings/video.svg diff --git a/UI/forms/images/sources/brush.svg b/frontend/forms/images/sources/brush.svg similarity index 100% rename from UI/forms/images/sources/brush.svg rename to frontend/forms/images/sources/brush.svg diff --git a/UI/forms/images/sources/camera.svg b/frontend/forms/images/sources/camera.svg similarity index 100% rename from UI/forms/images/sources/camera.svg rename to frontend/forms/images/sources/camera.svg diff --git a/UI/forms/images/sources/default.svg b/frontend/forms/images/sources/default.svg similarity index 100% rename from UI/forms/images/sources/default.svg rename to frontend/forms/images/sources/default.svg diff --git a/UI/forms/images/sources/gamepad.svg b/frontend/forms/images/sources/gamepad.svg similarity index 100% rename from UI/forms/images/sources/gamepad.svg rename to frontend/forms/images/sources/gamepad.svg diff --git a/UI/forms/images/sources/globe.svg b/frontend/forms/images/sources/globe.svg similarity index 100% rename from UI/forms/images/sources/globe.svg rename to frontend/forms/images/sources/globe.svg diff --git a/UI/forms/images/sources/group.svg b/frontend/forms/images/sources/group.svg similarity index 100% rename from UI/forms/images/sources/group.svg rename to frontend/forms/images/sources/group.svg diff --git a/UI/forms/images/sources/image.svg b/frontend/forms/images/sources/image.svg similarity index 100% rename from UI/forms/images/sources/image.svg rename to frontend/forms/images/sources/image.svg diff --git a/UI/forms/images/sources/media.svg b/frontend/forms/images/sources/media.svg similarity index 100% rename from UI/forms/images/sources/media.svg rename to frontend/forms/images/sources/media.svg diff --git a/UI/forms/images/sources/microphone.svg b/frontend/forms/images/sources/microphone.svg similarity index 100% rename from UI/forms/images/sources/microphone.svg rename to frontend/forms/images/sources/microphone.svg diff --git a/UI/forms/images/sources/scene.svg b/frontend/forms/images/sources/scene.svg similarity index 100% rename from UI/forms/images/sources/scene.svg rename to frontend/forms/images/sources/scene.svg diff --git a/UI/forms/images/sources/slideshow.svg b/frontend/forms/images/sources/slideshow.svg similarity index 100% rename from UI/forms/images/sources/slideshow.svg rename to frontend/forms/images/sources/slideshow.svg diff --git a/UI/forms/images/sources/text.svg b/frontend/forms/images/sources/text.svg similarity index 100% rename from UI/forms/images/sources/text.svg rename to frontend/forms/images/sources/text.svg diff --git a/UI/forms/images/sources/window.svg b/frontend/forms/images/sources/window.svg similarity index 100% rename from UI/forms/images/sources/window.svg rename to frontend/forms/images/sources/window.svg diff --git a/UI/forms/images/sources/windowaudio.svg b/frontend/forms/images/sources/windowaudio.svg similarity index 100% rename from UI/forms/images/sources/windowaudio.svg rename to frontend/forms/images/sources/windowaudio.svg diff --git a/UI/forms/images/streaming-active.svg b/frontend/forms/images/streaming-active.svg similarity index 100% rename from UI/forms/images/streaming-active.svg rename to frontend/forms/images/streaming-active.svg diff --git a/UI/forms/images/streaming-inactive.svg b/frontend/forms/images/streaming-inactive.svg similarity index 100% rename from UI/forms/images/streaming-inactive.svg rename to frontend/forms/images/streaming-inactive.svg diff --git a/UI/forms/images/trash.svg b/frontend/forms/images/trash.svg similarity index 100% rename from UI/forms/images/trash.svg rename to frontend/forms/images/trash.svg diff --git a/UI/forms/images/tray_active.png b/frontend/forms/images/tray_active.png similarity index 100% rename from UI/forms/images/tray_active.png rename to frontend/forms/images/tray_active.png diff --git a/UI/forms/images/tray_active_macos.png b/frontend/forms/images/tray_active_macos.png similarity index 100% rename from UI/forms/images/tray_active_macos.png rename to frontend/forms/images/tray_active_macos.png diff --git a/UI/forms/images/tray_active_macos.svg b/frontend/forms/images/tray_active_macos.svg similarity index 100% rename from UI/forms/images/tray_active_macos.svg rename to frontend/forms/images/tray_active_macos.svg diff --git a/UI/forms/images/unassigned.svg b/frontend/forms/images/unassigned.svg similarity index 100% rename from UI/forms/images/unassigned.svg rename to frontend/forms/images/unassigned.svg diff --git a/UI/forms/images/unlocked.svg b/frontend/forms/images/unlocked.svg similarity index 100% rename from UI/forms/images/unlocked.svg rename to frontend/forms/images/unlocked.svg diff --git a/UI/forms/images/up.svg b/frontend/forms/images/up.svg similarity index 100% rename from UI/forms/images/up.svg rename to frontend/forms/images/up.svg diff --git a/UI/forms/images/visible.svg b/frontend/forms/images/visible.svg similarity index 100% rename from UI/forms/images/visible.svg rename to frontend/forms/images/visible.svg diff --git a/UI/forms/images/warning.svg b/frontend/forms/images/warning.svg similarity index 100% rename from UI/forms/images/warning.svg rename to frontend/forms/images/warning.svg diff --git a/UI/forms/obs.qrc b/frontend/forms/obs.qrc similarity index 100% rename from UI/forms/obs.qrc rename to frontend/forms/obs.qrc diff --git a/UI/forms/source-toolbar/browser-source-toolbar.ui b/frontend/forms/source-toolbar/browser-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/browser-source-toolbar.ui rename to frontend/forms/source-toolbar/browser-source-toolbar.ui diff --git a/UI/forms/source-toolbar/color-source-toolbar.ui b/frontend/forms/source-toolbar/color-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/color-source-toolbar.ui rename to frontend/forms/source-toolbar/color-source-toolbar.ui diff --git a/UI/forms/source-toolbar/device-select-toolbar.ui b/frontend/forms/source-toolbar/device-select-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/device-select-toolbar.ui rename to frontend/forms/source-toolbar/device-select-toolbar.ui diff --git a/UI/forms/source-toolbar/game-capture-toolbar.ui b/frontend/forms/source-toolbar/game-capture-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/game-capture-toolbar.ui rename to frontend/forms/source-toolbar/game-capture-toolbar.ui diff --git a/UI/forms/source-toolbar/image-source-toolbar.ui b/frontend/forms/source-toolbar/image-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/image-source-toolbar.ui rename to frontend/forms/source-toolbar/image-source-toolbar.ui diff --git a/UI/forms/source-toolbar/media-controls.ui b/frontend/forms/source-toolbar/media-controls.ui similarity index 98% rename from UI/forms/source-toolbar/media-controls.ui rename to frontend/forms/source-toolbar/media-controls.ui index 1a2a47f2b..b6886e7e2 100644 --- a/UI/forms/source-toolbar/media-controls.ui +++ b/frontend/forms/source-toolbar/media-controls.ui @@ -285,12 +285,12 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
AbsoluteSlider QSlider -
absolute-slider.hpp
+
components/AbsoluteSlider.hpp
diff --git a/UI/forms/source-toolbar/text-source-toolbar.ui b/frontend/forms/source-toolbar/text-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/text-source-toolbar.ui rename to frontend/forms/source-toolbar/text-source-toolbar.ui From 47613a0927c828c3bc3dfffa79cbd33e70f1dbbf Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:22:32 +0100 Subject: [PATCH 33/37] frontend: Migrate frontend plugins --- .../plugins}/CMakeLists.txt | 0 .../plugins}/aja-output-ui/AJAOutputUI.cpp | 0 .../plugins}/aja-output-ui/AJAOutputUI.h | 0 .../plugins}/aja-output-ui/CMakeLists.txt | 0 .../plugins}/aja-output-ui/aja-ui-main.cpp | 0 .../plugins}/aja-output-ui/aja-ui-main.h | 0 .../aja-output-ui/cmake/windows/obs-module.rc.in | 0 .../plugins}/aja-output-ui/data/locale/af-ZA.ini | 0 .../plugins}/aja-output-ui/data/locale/ar-SA.ini | 0 .../plugins}/aja-output-ui/data/locale/az-AZ.ini | 0 .../plugins}/aja-output-ui/data/locale/be-BY.ini | 0 .../plugins}/aja-output-ui/data/locale/bg-BG.ini | 0 .../plugins}/aja-output-ui/data/locale/ca-ES.ini | 0 .../plugins}/aja-output-ui/data/locale/cs-CZ.ini | 0 .../plugins}/aja-output-ui/data/locale/da-DK.ini | 0 .../plugins}/aja-output-ui/data/locale/de-DE.ini | 0 .../plugins}/aja-output-ui/data/locale/el-GR.ini | 0 .../plugins}/aja-output-ui/data/locale/en-GB.ini | 0 .../plugins}/aja-output-ui/data/locale/en-US.ini | 0 .../plugins}/aja-output-ui/data/locale/es-ES.ini | 0 .../plugins}/aja-output-ui/data/locale/et-EE.ini | 0 .../plugins}/aja-output-ui/data/locale/eu-ES.ini | 0 .../plugins}/aja-output-ui/data/locale/fa-IR.ini | 0 .../plugins}/aja-output-ui/data/locale/fi-FI.ini | 0 .../plugins}/aja-output-ui/data/locale/fil-PH.ini | 0 .../plugins}/aja-output-ui/data/locale/fr-FR.ini | 0 .../plugins}/aja-output-ui/data/locale/gl-ES.ini | 0 .../plugins}/aja-output-ui/data/locale/he-IL.ini | 0 .../plugins}/aja-output-ui/data/locale/hi-IN.ini | 0 .../plugins}/aja-output-ui/data/locale/hr-HR.ini | 0 .../plugins}/aja-output-ui/data/locale/hu-HU.ini | 0 .../plugins}/aja-output-ui/data/locale/hy-AM.ini | 0 .../plugins}/aja-output-ui/data/locale/id-ID.ini | 0 .../plugins}/aja-output-ui/data/locale/it-IT.ini | 0 .../plugins}/aja-output-ui/data/locale/ja-JP.ini | 0 .../plugins}/aja-output-ui/data/locale/ka-GE.ini | 0 .../plugins}/aja-output-ui/data/locale/kaa.ini | 0 .../plugins}/aja-output-ui/data/locale/kmr-TR.ini | 0 .../plugins}/aja-output-ui/data/locale/ko-KR.ini | 0 .../plugins}/aja-output-ui/data/locale/ms-MY.ini | 0 .../plugins}/aja-output-ui/data/locale/nb-NO.ini | 0 .../plugins}/aja-output-ui/data/locale/nl-NL.ini | 0 .../plugins}/aja-output-ui/data/locale/pl-PL.ini | 0 .../plugins}/aja-output-ui/data/locale/pt-BR.ini | 0 .../plugins}/aja-output-ui/data/locale/pt-PT.ini | 0 .../plugins}/aja-output-ui/data/locale/ro-RO.ini | 0 .../plugins}/aja-output-ui/data/locale/ru-RU.ini | 0 .../plugins}/aja-output-ui/data/locale/si-LK.ini | 0 .../plugins}/aja-output-ui/data/locale/sk-SK.ini | 0 .../plugins}/aja-output-ui/data/locale/sl-SI.ini | 0 .../plugins}/aja-output-ui/data/locale/sv-SE.ini | 0 .../plugins}/aja-output-ui/data/locale/th-TH.ini | 0 .../plugins}/aja-output-ui/data/locale/tl-PH.ini | 0 .../plugins}/aja-output-ui/data/locale/tr-TR.ini | 0 .../plugins}/aja-output-ui/data/locale/tt-RU.ini | 0 .../plugins}/aja-output-ui/data/locale/ug-CN.ini | 0 .../plugins}/aja-output-ui/data/locale/uk-UA.ini | 0 .../plugins}/aja-output-ui/data/locale/ur-PK.ini | 0 .../plugins}/aja-output-ui/data/locale/vi-VN.ini | 0 .../plugins}/aja-output-ui/data/locale/zh-CN.ini | 0 .../plugins}/aja-output-ui/data/locale/zh-TW.ini | 0 .../plugins}/aja-output-ui/forms/output.ui | 0 .../plugins}/decklink-captions/CMakeLists.txt | 0 .../cmake/windows/obs-module.rc.in | 0 .../plugins}/decklink-captions/data/.keepme | 0 .../decklink-captions/decklink-captions.cpp | 0 .../plugins}/decklink-captions/decklink-captions.h | 0 .../plugins}/decklink-captions/forms/captions.ui | 0 .../plugins}/decklink-output-ui/CMakeLists.txt | 0 .../decklink-output-ui/DecklinkOutputUI.cpp | 0 .../plugins}/decklink-output-ui/DecklinkOutputUI.h | 0 .../cmake/windows/obs-module.rc.in | 0 .../plugins}/decklink-output-ui/data/.keepme | 0 .../decklink-output-ui/decklink-ui-main.cpp | 0 .../plugins}/decklink-output-ui/decklink-ui-main.h | 0 .../plugins}/decklink-output-ui/forms/output.ui | 0 .../plugins}/frontend-tools/CMakeLists.txt | 0 .../frontend-tools/auto-scene-switcher-nix.cpp | 0 .../frontend-tools/auto-scene-switcher-osx.mm | 0 .../frontend-tools/auto-scene-switcher-win.cpp | 0 .../plugins}/frontend-tools/auto-scene-switcher.cpp | 0 .../plugins}/frontend-tools/auto-scene-switcher.hpp | 0 .../plugins}/frontend-tools/captions-handler.cpp | 0 .../plugins}/frontend-tools/captions-handler.hpp | 0 .../frontend-tools/captions-mssapi-stream.cpp | 0 .../frontend-tools/captions-mssapi-stream.hpp | 0 .../plugins}/frontend-tools/captions-mssapi.cpp | 0 .../plugins}/frontend-tools/captions-mssapi.hpp | 0 .../plugins}/frontend-tools/captions.cpp | 0 .../plugins}/frontend-tools/captions.hpp | 0 .../frontend-tools/cmake/windows/obs-module.rc.in | 0 .../plugins}/frontend-tools/data/locale/af-ZA.ini | 0 .../plugins}/frontend-tools/data/locale/an-ES.ini | 0 .../plugins}/frontend-tools/data/locale/ar-SA.ini | 0 .../plugins}/frontend-tools/data/locale/az-AZ.ini | 0 .../plugins}/frontend-tools/data/locale/ba-RU.ini | 0 .../plugins}/frontend-tools/data/locale/be-BY.ini | 0 .../plugins}/frontend-tools/data/locale/bg-BG.ini | 0 .../plugins}/frontend-tools/data/locale/bn-BD.ini | 0 .../plugins}/frontend-tools/data/locale/ca-ES.ini | 0 .../plugins}/frontend-tools/data/locale/cs-CZ.ini | 0 .../plugins}/frontend-tools/data/locale/da-DK.ini | 0 .../plugins}/frontend-tools/data/locale/de-DE.ini | 0 .../plugins}/frontend-tools/data/locale/el-GR.ini | 0 .../plugins}/frontend-tools/data/locale/en-GB.ini | 0 .../plugins}/frontend-tools/data/locale/en-US.ini | 0 .../plugins}/frontend-tools/data/locale/eo-UY.ini | 0 .../plugins}/frontend-tools/data/locale/es-ES.ini | 0 .../plugins}/frontend-tools/data/locale/et-EE.ini | 0 .../plugins}/frontend-tools/data/locale/eu-ES.ini | 0 .../plugins}/frontend-tools/data/locale/fa-IR.ini | 0 .../plugins}/frontend-tools/data/locale/fi-FI.ini | 0 .../plugins}/frontend-tools/data/locale/fil-PH.ini | 0 .../plugins}/frontend-tools/data/locale/fr-FR.ini | 0 .../plugins}/frontend-tools/data/locale/gd-GB.ini | 0 .../plugins}/frontend-tools/data/locale/gl-ES.ini | 0 .../plugins}/frontend-tools/data/locale/he-IL.ini | 0 .../plugins}/frontend-tools/data/locale/hi-IN.ini | 0 .../plugins}/frontend-tools/data/locale/hr-HR.ini | 0 .../plugins}/frontend-tools/data/locale/hu-HU.ini | 0 .../plugins}/frontend-tools/data/locale/hy-AM.ini | 0 .../plugins}/frontend-tools/data/locale/id-ID.ini | 0 .../plugins}/frontend-tools/data/locale/is-IS.ini | 0 .../plugins}/frontend-tools/data/locale/it-IT.ini | 0 .../plugins}/frontend-tools/data/locale/ja-JP.ini | 0 .../plugins}/frontend-tools/data/locale/ka-GE.ini | 0 .../plugins}/frontend-tools/data/locale/kaa.ini | 0 .../plugins}/frontend-tools/data/locale/kab-KAB.ini | 0 .../plugins}/frontend-tools/data/locale/kmr-TR.ini | 0 .../plugins}/frontend-tools/data/locale/ko-KR.ini | 0 .../plugins}/frontend-tools/data/locale/lo-LA.ini | 0 .../plugins}/frontend-tools/data/locale/lt-LT.ini | 0 .../plugins}/frontend-tools/data/locale/mn-MN.ini | 0 .../plugins}/frontend-tools/data/locale/ms-MY.ini | 0 .../plugins}/frontend-tools/data/locale/nb-NO.ini | 0 .../plugins}/frontend-tools/data/locale/nl-NL.ini | 0 .../plugins}/frontend-tools/data/locale/nn-NO.ini | 0 .../plugins}/frontend-tools/data/locale/oc-FR.ini | 0 .../plugins}/frontend-tools/data/locale/pl-PL.ini | 0 .../plugins}/frontend-tools/data/locale/pt-BR.ini | 0 .../plugins}/frontend-tools/data/locale/pt-PT.ini | 0 .../plugins}/frontend-tools/data/locale/ro-RO.ini | 0 .../plugins}/frontend-tools/data/locale/ru-RU.ini | 0 .../plugins}/frontend-tools/data/locale/si-LK.ini | 0 .../plugins}/frontend-tools/data/locale/sk-SK.ini | 0 .../plugins}/frontend-tools/data/locale/sl-SI.ini | 0 .../plugins}/frontend-tools/data/locale/sq-AL.ini | 0 .../plugins}/frontend-tools/data/locale/sr-CS.ini | 0 .../plugins}/frontend-tools/data/locale/sr-SP.ini | 0 .../plugins}/frontend-tools/data/locale/sv-SE.ini | 0 .../plugins}/frontend-tools/data/locale/szl-PL.ini | 0 .../plugins}/frontend-tools/data/locale/ta-IN.ini | 0 .../plugins}/frontend-tools/data/locale/th-TH.ini | 0 .../plugins}/frontend-tools/data/locale/tl-PH.ini | 0 .../plugins}/frontend-tools/data/locale/tr-TR.ini | 0 .../plugins}/frontend-tools/data/locale/tt-RU.ini | 0 .../plugins}/frontend-tools/data/locale/ug-CN.ini | 0 .../plugins}/frontend-tools/data/locale/uk-UA.ini | 0 .../plugins}/frontend-tools/data/locale/vi-VN.ini | 0 .../plugins}/frontend-tools/data/locale/zh-CN.ini | 0 .../plugins}/frontend-tools/data/locale/zh-TW.ini | 0 .../frontend-tools/data/scripts/clock-source.lua | 0 .../data/scripts/clock-source/dial.png | Bin .../data/scripts/clock-source/hour.png | Bin .../data/scripts/clock-source/minute.png | Bin .../data/scripts/clock-source/second.png | Bin .../frontend-tools/data/scripts/countdown.lua | 0 .../frontend-tools/data/scripts/instant-replay.lua | 0 .../frontend-tools/data/scripts/pause-scene.lua | 0 .../frontend-tools/data/scripts/url-text.py | 0 .../frontend-tools/forms/auto-scene-switcher.ui | 0 .../plugins}/frontend-tools/forms/captions.ui | 0 .../plugins}/frontend-tools/forms/output-timer.ui | 0 .../plugins}/frontend-tools/forms/scripts.ui | 0 .../frontend-tools/frontend-tools-config.h.in | 0 .../plugins}/frontend-tools/frontend-tools.c | 0 .../plugins}/frontend-tools/output-timer.cpp | 0 .../plugins}/frontend-tools/output-timer.hpp | 0 .../plugins}/frontend-tools/scripts.cpp | 0 .../plugins}/frontend-tools/scripts.hpp | 0 .../plugins}/frontend-tools/tool-helpers.hpp | 0 181 files changed, 0 insertions(+), 0 deletions(-) rename {UI/frontend-plugins => frontend/plugins}/CMakeLists.txt (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/AJAOutputUI.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/AJAOutputUI.h (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/CMakeLists.txt (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/aja-ui-main.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/aja-ui-main.h (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/cmake/windows/obs-module.rc.in (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/af-ZA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ar-SA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/az-AZ.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/be-BY.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/bg-BG.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ca-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/cs-CZ.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/da-DK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/de-DE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/el-GR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/en-GB.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/en-US.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/es-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/et-EE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/eu-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/fa-IR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/fi-FI.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/fil-PH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/fr-FR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/gl-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/he-IL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/hi-IN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/hr-HR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/hu-HU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/hy-AM.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/id-ID.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/it-IT.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ja-JP.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ka-GE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/kaa.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/kmr-TR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ko-KR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ms-MY.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/nb-NO.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/nl-NL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/pl-PL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/pt-BR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/pt-PT.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ro-RO.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ru-RU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/si-LK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/sk-SK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/sl-SI.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/sv-SE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/th-TH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/tl-PH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/tr-TR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/tt-RU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ug-CN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/uk-UA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/ur-PK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/vi-VN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/zh-CN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/data/locale/zh-TW.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/aja-output-ui/forms/output.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/CMakeLists.txt (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/cmake/windows/obs-module.rc.in (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/data/.keepme (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/decklink-captions.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/decklink-captions.h (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-captions/forms/captions.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/CMakeLists.txt (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/DecklinkOutputUI.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/DecklinkOutputUI.h (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/cmake/windows/obs-module.rc.in (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/data/.keepme (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/decklink-ui-main.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/decklink-ui-main.h (100%) rename {UI/frontend-plugins => frontend/plugins}/decklink-output-ui/forms/output.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/CMakeLists.txt (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/auto-scene-switcher-nix.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/auto-scene-switcher-osx.mm (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/auto-scene-switcher-win.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/auto-scene-switcher.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/auto-scene-switcher.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-handler.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-handler.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-mssapi-stream.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-mssapi-stream.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-mssapi.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions-mssapi.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/captions.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/cmake/windows/obs-module.rc.in (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/af-ZA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/an-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ar-SA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/az-AZ.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ba-RU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/be-BY.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/bg-BG.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/bn-BD.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ca-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/cs-CZ.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/da-DK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/de-DE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/el-GR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/en-GB.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/en-US.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/eo-UY.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/es-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/et-EE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/eu-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/fa-IR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/fi-FI.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/fil-PH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/fr-FR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/gd-GB.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/gl-ES.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/he-IL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/hi-IN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/hr-HR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/hu-HU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/hy-AM.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/id-ID.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/is-IS.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/it-IT.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ja-JP.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ka-GE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/kaa.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/kab-KAB.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/kmr-TR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ko-KR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/lo-LA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/lt-LT.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/mn-MN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ms-MY.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/nb-NO.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/nl-NL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/nn-NO.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/oc-FR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/pl-PL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/pt-BR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/pt-PT.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ro-RO.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ru-RU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/si-LK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sk-SK.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sl-SI.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sq-AL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sr-CS.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sr-SP.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/sv-SE.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/szl-PL.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ta-IN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/th-TH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/tl-PH.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/tr-TR.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/tt-RU.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/ug-CN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/uk-UA.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/vi-VN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/zh-CN.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/locale/zh-TW.ini (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/clock-source.lua (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/clock-source/dial.png (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/clock-source/hour.png (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/clock-source/minute.png (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/clock-source/second.png (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/countdown.lua (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/instant-replay.lua (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/pause-scene.lua (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/data/scripts/url-text.py (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/forms/auto-scene-switcher.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/forms/captions.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/forms/output-timer.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/forms/scripts.ui (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/frontend-tools-config.h.in (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/frontend-tools.c (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/output-timer.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/output-timer.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/scripts.cpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/scripts.hpp (100%) rename {UI/frontend-plugins => frontend/plugins}/frontend-tools/tool-helpers.hpp (100%) diff --git a/UI/frontend-plugins/CMakeLists.txt b/frontend/plugins/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/CMakeLists.txt rename to frontend/plugins/CMakeLists.txt diff --git a/UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp b/frontend/plugins/aja-output-ui/AJAOutputUI.cpp similarity index 100% rename from UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp rename to frontend/plugins/aja-output-ui/AJAOutputUI.cpp diff --git a/UI/frontend-plugins/aja-output-ui/AJAOutputUI.h b/frontend/plugins/aja-output-ui/AJAOutputUI.h similarity index 100% rename from UI/frontend-plugins/aja-output-ui/AJAOutputUI.h rename to frontend/plugins/aja-output-ui/AJAOutputUI.h diff --git a/UI/frontend-plugins/aja-output-ui/CMakeLists.txt b/frontend/plugins/aja-output-ui/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/aja-output-ui/CMakeLists.txt rename to frontend/plugins/aja-output-ui/CMakeLists.txt diff --git a/UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp b/frontend/plugins/aja-output-ui/aja-ui-main.cpp similarity index 100% rename from UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp rename to frontend/plugins/aja-output-ui/aja-ui-main.cpp diff --git a/UI/frontend-plugins/aja-output-ui/aja-ui-main.h b/frontend/plugins/aja-output-ui/aja-ui-main.h similarity index 100% rename from UI/frontend-plugins/aja-output-ui/aja-ui-main.h rename to frontend/plugins/aja-output-ui/aja-ui-main.h diff --git a/UI/frontend-plugins/aja-output-ui/cmake/windows/obs-module.rc.in b/frontend/plugins/aja-output-ui/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/aja-output-ui/cmake/windows/obs-module.rc.in rename to frontend/plugins/aja-output-ui/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/af-ZA.ini b/frontend/plugins/aja-output-ui/data/locale/af-ZA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/af-ZA.ini rename to frontend/plugins/aja-output-ui/data/locale/af-ZA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ar-SA.ini b/frontend/plugins/aja-output-ui/data/locale/ar-SA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ar-SA.ini rename to frontend/plugins/aja-output-ui/data/locale/ar-SA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/az-AZ.ini b/frontend/plugins/aja-output-ui/data/locale/az-AZ.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/az-AZ.ini rename to frontend/plugins/aja-output-ui/data/locale/az-AZ.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/be-BY.ini b/frontend/plugins/aja-output-ui/data/locale/be-BY.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/be-BY.ini rename to frontend/plugins/aja-output-ui/data/locale/be-BY.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/bg-BG.ini b/frontend/plugins/aja-output-ui/data/locale/bg-BG.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/bg-BG.ini rename to frontend/plugins/aja-output-ui/data/locale/bg-BG.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ca-ES.ini b/frontend/plugins/aja-output-ui/data/locale/ca-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ca-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/ca-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/cs-CZ.ini b/frontend/plugins/aja-output-ui/data/locale/cs-CZ.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/cs-CZ.ini rename to frontend/plugins/aja-output-ui/data/locale/cs-CZ.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/da-DK.ini b/frontend/plugins/aja-output-ui/data/locale/da-DK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/da-DK.ini rename to frontend/plugins/aja-output-ui/data/locale/da-DK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/de-DE.ini b/frontend/plugins/aja-output-ui/data/locale/de-DE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/de-DE.ini rename to frontend/plugins/aja-output-ui/data/locale/de-DE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/el-GR.ini b/frontend/plugins/aja-output-ui/data/locale/el-GR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/el-GR.ini rename to frontend/plugins/aja-output-ui/data/locale/el-GR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/en-GB.ini b/frontend/plugins/aja-output-ui/data/locale/en-GB.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/en-GB.ini rename to frontend/plugins/aja-output-ui/data/locale/en-GB.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini b/frontend/plugins/aja-output-ui/data/locale/en-US.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini rename to frontend/plugins/aja-output-ui/data/locale/en-US.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/es-ES.ini b/frontend/plugins/aja-output-ui/data/locale/es-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/es-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/es-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/et-EE.ini b/frontend/plugins/aja-output-ui/data/locale/et-EE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/et-EE.ini rename to frontend/plugins/aja-output-ui/data/locale/et-EE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/eu-ES.ini b/frontend/plugins/aja-output-ui/data/locale/eu-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/eu-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/eu-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fa-IR.ini b/frontend/plugins/aja-output-ui/data/locale/fa-IR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fa-IR.ini rename to frontend/plugins/aja-output-ui/data/locale/fa-IR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fi-FI.ini b/frontend/plugins/aja-output-ui/data/locale/fi-FI.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fi-FI.ini rename to frontend/plugins/aja-output-ui/data/locale/fi-FI.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fil-PH.ini b/frontend/plugins/aja-output-ui/data/locale/fil-PH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fil-PH.ini rename to frontend/plugins/aja-output-ui/data/locale/fil-PH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fr-FR.ini b/frontend/plugins/aja-output-ui/data/locale/fr-FR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fr-FR.ini rename to frontend/plugins/aja-output-ui/data/locale/fr-FR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/gl-ES.ini b/frontend/plugins/aja-output-ui/data/locale/gl-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/gl-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/gl-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/he-IL.ini b/frontend/plugins/aja-output-ui/data/locale/he-IL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/he-IL.ini rename to frontend/plugins/aja-output-ui/data/locale/he-IL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hi-IN.ini b/frontend/plugins/aja-output-ui/data/locale/hi-IN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hi-IN.ini rename to frontend/plugins/aja-output-ui/data/locale/hi-IN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hr-HR.ini b/frontend/plugins/aja-output-ui/data/locale/hr-HR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hr-HR.ini rename to frontend/plugins/aja-output-ui/data/locale/hr-HR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hu-HU.ini b/frontend/plugins/aja-output-ui/data/locale/hu-HU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hu-HU.ini rename to frontend/plugins/aja-output-ui/data/locale/hu-HU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hy-AM.ini b/frontend/plugins/aja-output-ui/data/locale/hy-AM.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hy-AM.ini rename to frontend/plugins/aja-output-ui/data/locale/hy-AM.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/id-ID.ini b/frontend/plugins/aja-output-ui/data/locale/id-ID.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/id-ID.ini rename to frontend/plugins/aja-output-ui/data/locale/id-ID.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/it-IT.ini b/frontend/plugins/aja-output-ui/data/locale/it-IT.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/it-IT.ini rename to frontend/plugins/aja-output-ui/data/locale/it-IT.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ja-JP.ini b/frontend/plugins/aja-output-ui/data/locale/ja-JP.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ja-JP.ini rename to frontend/plugins/aja-output-ui/data/locale/ja-JP.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ka-GE.ini b/frontend/plugins/aja-output-ui/data/locale/ka-GE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ka-GE.ini rename to frontend/plugins/aja-output-ui/data/locale/ka-GE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/kaa.ini b/frontend/plugins/aja-output-ui/data/locale/kaa.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/kaa.ini rename to frontend/plugins/aja-output-ui/data/locale/kaa.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/kmr-TR.ini b/frontend/plugins/aja-output-ui/data/locale/kmr-TR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/kmr-TR.ini rename to frontend/plugins/aja-output-ui/data/locale/kmr-TR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ko-KR.ini b/frontend/plugins/aja-output-ui/data/locale/ko-KR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ko-KR.ini rename to frontend/plugins/aja-output-ui/data/locale/ko-KR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ms-MY.ini b/frontend/plugins/aja-output-ui/data/locale/ms-MY.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ms-MY.ini rename to frontend/plugins/aja-output-ui/data/locale/ms-MY.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/nb-NO.ini b/frontend/plugins/aja-output-ui/data/locale/nb-NO.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/nb-NO.ini rename to frontend/plugins/aja-output-ui/data/locale/nb-NO.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/nl-NL.ini b/frontend/plugins/aja-output-ui/data/locale/nl-NL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/nl-NL.ini rename to frontend/plugins/aja-output-ui/data/locale/nl-NL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pl-PL.ini b/frontend/plugins/aja-output-ui/data/locale/pl-PL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pl-PL.ini rename to frontend/plugins/aja-output-ui/data/locale/pl-PL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pt-BR.ini b/frontend/plugins/aja-output-ui/data/locale/pt-BR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pt-BR.ini rename to frontend/plugins/aja-output-ui/data/locale/pt-BR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pt-PT.ini b/frontend/plugins/aja-output-ui/data/locale/pt-PT.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pt-PT.ini rename to frontend/plugins/aja-output-ui/data/locale/pt-PT.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ro-RO.ini b/frontend/plugins/aja-output-ui/data/locale/ro-RO.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ro-RO.ini rename to frontend/plugins/aja-output-ui/data/locale/ro-RO.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ru-RU.ini b/frontend/plugins/aja-output-ui/data/locale/ru-RU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ru-RU.ini rename to frontend/plugins/aja-output-ui/data/locale/ru-RU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/si-LK.ini b/frontend/plugins/aja-output-ui/data/locale/si-LK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/si-LK.ini rename to frontend/plugins/aja-output-ui/data/locale/si-LK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sk-SK.ini b/frontend/plugins/aja-output-ui/data/locale/sk-SK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sk-SK.ini rename to frontend/plugins/aja-output-ui/data/locale/sk-SK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sl-SI.ini b/frontend/plugins/aja-output-ui/data/locale/sl-SI.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sl-SI.ini rename to frontend/plugins/aja-output-ui/data/locale/sl-SI.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sv-SE.ini b/frontend/plugins/aja-output-ui/data/locale/sv-SE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sv-SE.ini rename to frontend/plugins/aja-output-ui/data/locale/sv-SE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/th-TH.ini b/frontend/plugins/aja-output-ui/data/locale/th-TH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/th-TH.ini rename to frontend/plugins/aja-output-ui/data/locale/th-TH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tl-PH.ini b/frontend/plugins/aja-output-ui/data/locale/tl-PH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tl-PH.ini rename to frontend/plugins/aja-output-ui/data/locale/tl-PH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tr-TR.ini b/frontend/plugins/aja-output-ui/data/locale/tr-TR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tr-TR.ini rename to frontend/plugins/aja-output-ui/data/locale/tr-TR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tt-RU.ini b/frontend/plugins/aja-output-ui/data/locale/tt-RU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tt-RU.ini rename to frontend/plugins/aja-output-ui/data/locale/tt-RU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ug-CN.ini b/frontend/plugins/aja-output-ui/data/locale/ug-CN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ug-CN.ini rename to frontend/plugins/aja-output-ui/data/locale/ug-CN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/uk-UA.ini b/frontend/plugins/aja-output-ui/data/locale/uk-UA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/uk-UA.ini rename to frontend/plugins/aja-output-ui/data/locale/uk-UA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ur-PK.ini b/frontend/plugins/aja-output-ui/data/locale/ur-PK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ur-PK.ini rename to frontend/plugins/aja-output-ui/data/locale/ur-PK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/vi-VN.ini b/frontend/plugins/aja-output-ui/data/locale/vi-VN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/vi-VN.ini rename to frontend/plugins/aja-output-ui/data/locale/vi-VN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/zh-CN.ini b/frontend/plugins/aja-output-ui/data/locale/zh-CN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/zh-CN.ini rename to frontend/plugins/aja-output-ui/data/locale/zh-CN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/zh-TW.ini b/frontend/plugins/aja-output-ui/data/locale/zh-TW.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/zh-TW.ini rename to frontend/plugins/aja-output-ui/data/locale/zh-TW.ini diff --git a/UI/frontend-plugins/aja-output-ui/forms/output.ui b/frontend/plugins/aja-output-ui/forms/output.ui similarity index 100% rename from UI/frontend-plugins/aja-output-ui/forms/output.ui rename to frontend/plugins/aja-output-ui/forms/output.ui diff --git a/UI/frontend-plugins/decklink-captions/CMakeLists.txt b/frontend/plugins/decklink-captions/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/decklink-captions/CMakeLists.txt rename to frontend/plugins/decklink-captions/CMakeLists.txt diff --git a/UI/frontend-plugins/decklink-captions/cmake/windows/obs-module.rc.in b/frontend/plugins/decklink-captions/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/decklink-captions/cmake/windows/obs-module.rc.in rename to frontend/plugins/decklink-captions/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/decklink-captions/data/.keepme b/frontend/plugins/decklink-captions/data/.keepme similarity index 100% rename from UI/frontend-plugins/decklink-captions/data/.keepme rename to frontend/plugins/decklink-captions/data/.keepme diff --git a/UI/frontend-plugins/decklink-captions/decklink-captions.cpp b/frontend/plugins/decklink-captions/decklink-captions.cpp similarity index 100% rename from UI/frontend-plugins/decklink-captions/decklink-captions.cpp rename to frontend/plugins/decklink-captions/decklink-captions.cpp diff --git a/UI/frontend-plugins/decklink-captions/decklink-captions.h b/frontend/plugins/decklink-captions/decklink-captions.h similarity index 100% rename from UI/frontend-plugins/decklink-captions/decklink-captions.h rename to frontend/plugins/decklink-captions/decklink-captions.h diff --git a/UI/frontend-plugins/decklink-captions/forms/captions.ui b/frontend/plugins/decklink-captions/forms/captions.ui similarity index 100% rename from UI/frontend-plugins/decklink-captions/forms/captions.ui rename to frontend/plugins/decklink-captions/forms/captions.ui diff --git a/UI/frontend-plugins/decklink-output-ui/CMakeLists.txt b/frontend/plugins/decklink-output-ui/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/CMakeLists.txt rename to frontend/plugins/decklink-output-ui/CMakeLists.txt diff --git a/UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.cpp b/frontend/plugins/decklink-output-ui/DecklinkOutputUI.cpp similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.cpp rename to frontend/plugins/decklink-output-ui/DecklinkOutputUI.cpp diff --git a/UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.h b/frontend/plugins/decklink-output-ui/DecklinkOutputUI.h similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.h rename to frontend/plugins/decklink-output-ui/DecklinkOutputUI.h diff --git a/UI/frontend-plugins/decklink-output-ui/cmake/windows/obs-module.rc.in b/frontend/plugins/decklink-output-ui/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/cmake/windows/obs-module.rc.in rename to frontend/plugins/decklink-output-ui/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/decklink-output-ui/data/.keepme b/frontend/plugins/decklink-output-ui/data/.keepme similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/data/.keepme rename to frontend/plugins/decklink-output-ui/data/.keepme diff --git a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp b/frontend/plugins/decklink-output-ui/decklink-ui-main.cpp similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp rename to frontend/plugins/decklink-output-ui/decklink-ui-main.cpp diff --git a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.h b/frontend/plugins/decklink-output-ui/decklink-ui-main.h similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/decklink-ui-main.h rename to frontend/plugins/decklink-output-ui/decklink-ui-main.h diff --git a/UI/frontend-plugins/decklink-output-ui/forms/output.ui b/frontend/plugins/decklink-output-ui/forms/output.ui similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/forms/output.ui rename to frontend/plugins/decklink-output-ui/forms/output.ui diff --git a/UI/frontend-plugins/frontend-tools/CMakeLists.txt b/frontend/plugins/frontend-tools/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/frontend-tools/CMakeLists.txt rename to frontend/plugins/frontend-tools/CMakeLists.txt diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-nix.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher-nix.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-nix.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher-nix.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-osx.mm b/frontend/plugins/frontend-tools/auto-scene-switcher-osx.mm similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-osx.mm rename to frontend/plugins/frontend-tools/auto-scene-switcher-osx.mm diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-win.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher-win.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-win.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher-win.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher.hpp b/frontend/plugins/frontend-tools/auto-scene-switcher.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher.hpp rename to frontend/plugins/frontend-tools/auto-scene-switcher.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-handler.cpp b/frontend/plugins/frontend-tools/captions-handler.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-handler.cpp rename to frontend/plugins/frontend-tools/captions-handler.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-handler.hpp b/frontend/plugins/frontend-tools/captions-handler.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-handler.hpp rename to frontend/plugins/frontend-tools/captions-handler.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi-stream.cpp b/frontend/plugins/frontend-tools/captions-mssapi-stream.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi-stream.cpp rename to frontend/plugins/frontend-tools/captions-mssapi-stream.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi-stream.hpp b/frontend/plugins/frontend-tools/captions-mssapi-stream.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi-stream.hpp rename to frontend/plugins/frontend-tools/captions-mssapi-stream.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi.cpp b/frontend/plugins/frontend-tools/captions-mssapi.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi.cpp rename to frontend/plugins/frontend-tools/captions-mssapi.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi.hpp b/frontend/plugins/frontend-tools/captions-mssapi.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi.hpp rename to frontend/plugins/frontend-tools/captions-mssapi.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions.cpp b/frontend/plugins/frontend-tools/captions.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions.cpp rename to frontend/plugins/frontend-tools/captions.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions.hpp b/frontend/plugins/frontend-tools/captions.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions.hpp rename to frontend/plugins/frontend-tools/captions.hpp diff --git a/UI/frontend-plugins/frontend-tools/cmake/windows/obs-module.rc.in b/frontend/plugins/frontend-tools/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/frontend-tools/cmake/windows/obs-module.rc.in rename to frontend/plugins/frontend-tools/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/frontend-tools/data/locale/af-ZA.ini b/frontend/plugins/frontend-tools/data/locale/af-ZA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/af-ZA.ini rename to frontend/plugins/frontend-tools/data/locale/af-ZA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/an-ES.ini b/frontend/plugins/frontend-tools/data/locale/an-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/an-ES.ini rename to frontend/plugins/frontend-tools/data/locale/an-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ar-SA.ini b/frontend/plugins/frontend-tools/data/locale/ar-SA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ar-SA.ini rename to frontend/plugins/frontend-tools/data/locale/ar-SA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/az-AZ.ini b/frontend/plugins/frontend-tools/data/locale/az-AZ.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/az-AZ.ini rename to frontend/plugins/frontend-tools/data/locale/az-AZ.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ba-RU.ini b/frontend/plugins/frontend-tools/data/locale/ba-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ba-RU.ini rename to frontend/plugins/frontend-tools/data/locale/ba-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/be-BY.ini b/frontend/plugins/frontend-tools/data/locale/be-BY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/be-BY.ini rename to frontend/plugins/frontend-tools/data/locale/be-BY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/bg-BG.ini b/frontend/plugins/frontend-tools/data/locale/bg-BG.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/bg-BG.ini rename to frontend/plugins/frontend-tools/data/locale/bg-BG.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/bn-BD.ini b/frontend/plugins/frontend-tools/data/locale/bn-BD.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/bn-BD.ini rename to frontend/plugins/frontend-tools/data/locale/bn-BD.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ca-ES.ini b/frontend/plugins/frontend-tools/data/locale/ca-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ca-ES.ini rename to frontend/plugins/frontend-tools/data/locale/ca-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/cs-CZ.ini b/frontend/plugins/frontend-tools/data/locale/cs-CZ.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/cs-CZ.ini rename to frontend/plugins/frontend-tools/data/locale/cs-CZ.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/da-DK.ini b/frontend/plugins/frontend-tools/data/locale/da-DK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/da-DK.ini rename to frontend/plugins/frontend-tools/data/locale/da-DK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/de-DE.ini b/frontend/plugins/frontend-tools/data/locale/de-DE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/de-DE.ini rename to frontend/plugins/frontend-tools/data/locale/de-DE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini b/frontend/plugins/frontend-tools/data/locale/el-GR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini rename to frontend/plugins/frontend-tools/data/locale/el-GR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-GB.ini b/frontend/plugins/frontend-tools/data/locale/en-GB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/en-GB.ini rename to frontend/plugins/frontend-tools/data/locale/en-GB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini b/frontend/plugins/frontend-tools/data/locale/en-US.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/en-US.ini rename to frontend/plugins/frontend-tools/data/locale/en-US.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/eo-UY.ini b/frontend/plugins/frontend-tools/data/locale/eo-UY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/eo-UY.ini rename to frontend/plugins/frontend-tools/data/locale/eo-UY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/es-ES.ini b/frontend/plugins/frontend-tools/data/locale/es-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/es-ES.ini rename to frontend/plugins/frontend-tools/data/locale/es-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/et-EE.ini b/frontend/plugins/frontend-tools/data/locale/et-EE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/et-EE.ini rename to frontend/plugins/frontend-tools/data/locale/et-EE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/eu-ES.ini b/frontend/plugins/frontend-tools/data/locale/eu-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/eu-ES.ini rename to frontend/plugins/frontend-tools/data/locale/eu-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fa-IR.ini b/frontend/plugins/frontend-tools/data/locale/fa-IR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fa-IR.ini rename to frontend/plugins/frontend-tools/data/locale/fa-IR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fi-FI.ini b/frontend/plugins/frontend-tools/data/locale/fi-FI.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fi-FI.ini rename to frontend/plugins/frontend-tools/data/locale/fi-FI.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fil-PH.ini b/frontend/plugins/frontend-tools/data/locale/fil-PH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fil-PH.ini rename to frontend/plugins/frontend-tools/data/locale/fil-PH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fr-FR.ini b/frontend/plugins/frontend-tools/data/locale/fr-FR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fr-FR.ini rename to frontend/plugins/frontend-tools/data/locale/fr-FR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini b/frontend/plugins/frontend-tools/data/locale/gd-GB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini rename to frontend/plugins/frontend-tools/data/locale/gd-GB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/gl-ES.ini b/frontend/plugins/frontend-tools/data/locale/gl-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/gl-ES.ini rename to frontend/plugins/frontend-tools/data/locale/gl-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/he-IL.ini b/frontend/plugins/frontend-tools/data/locale/he-IL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/he-IL.ini rename to frontend/plugins/frontend-tools/data/locale/he-IL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hi-IN.ini b/frontend/plugins/frontend-tools/data/locale/hi-IN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hi-IN.ini rename to frontend/plugins/frontend-tools/data/locale/hi-IN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hr-HR.ini b/frontend/plugins/frontend-tools/data/locale/hr-HR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hr-HR.ini rename to frontend/plugins/frontend-tools/data/locale/hr-HR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hu-HU.ini b/frontend/plugins/frontend-tools/data/locale/hu-HU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hu-HU.ini rename to frontend/plugins/frontend-tools/data/locale/hu-HU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hy-AM.ini b/frontend/plugins/frontend-tools/data/locale/hy-AM.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hy-AM.ini rename to frontend/plugins/frontend-tools/data/locale/hy-AM.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/id-ID.ini b/frontend/plugins/frontend-tools/data/locale/id-ID.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/id-ID.ini rename to frontend/plugins/frontend-tools/data/locale/id-ID.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/is-IS.ini b/frontend/plugins/frontend-tools/data/locale/is-IS.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/is-IS.ini rename to frontend/plugins/frontend-tools/data/locale/is-IS.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/it-IT.ini b/frontend/plugins/frontend-tools/data/locale/it-IT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/it-IT.ini rename to frontend/plugins/frontend-tools/data/locale/it-IT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ja-JP.ini b/frontend/plugins/frontend-tools/data/locale/ja-JP.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ja-JP.ini rename to frontend/plugins/frontend-tools/data/locale/ja-JP.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ka-GE.ini b/frontend/plugins/frontend-tools/data/locale/ka-GE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ka-GE.ini rename to frontend/plugins/frontend-tools/data/locale/ka-GE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kaa.ini b/frontend/plugins/frontend-tools/data/locale/kaa.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kaa.ini rename to frontend/plugins/frontend-tools/data/locale/kaa.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kab-KAB.ini b/frontend/plugins/frontend-tools/data/locale/kab-KAB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kab-KAB.ini rename to frontend/plugins/frontend-tools/data/locale/kab-KAB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kmr-TR.ini b/frontend/plugins/frontend-tools/data/locale/kmr-TR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kmr-TR.ini rename to frontend/plugins/frontend-tools/data/locale/kmr-TR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ko-KR.ini b/frontend/plugins/frontend-tools/data/locale/ko-KR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ko-KR.ini rename to frontend/plugins/frontend-tools/data/locale/ko-KR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/lo-LA.ini b/frontend/plugins/frontend-tools/data/locale/lo-LA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/lo-LA.ini rename to frontend/plugins/frontend-tools/data/locale/lo-LA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/lt-LT.ini b/frontend/plugins/frontend-tools/data/locale/lt-LT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/lt-LT.ini rename to frontend/plugins/frontend-tools/data/locale/lt-LT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/mn-MN.ini b/frontend/plugins/frontend-tools/data/locale/mn-MN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/mn-MN.ini rename to frontend/plugins/frontend-tools/data/locale/mn-MN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini b/frontend/plugins/frontend-tools/data/locale/ms-MY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini rename to frontend/plugins/frontend-tools/data/locale/ms-MY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nb-NO.ini b/frontend/plugins/frontend-tools/data/locale/nb-NO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nb-NO.ini rename to frontend/plugins/frontend-tools/data/locale/nb-NO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nl-NL.ini b/frontend/plugins/frontend-tools/data/locale/nl-NL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nl-NL.ini rename to frontend/plugins/frontend-tools/data/locale/nl-NL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nn-NO.ini b/frontend/plugins/frontend-tools/data/locale/nn-NO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nn-NO.ini rename to frontend/plugins/frontend-tools/data/locale/nn-NO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/oc-FR.ini b/frontend/plugins/frontend-tools/data/locale/oc-FR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/oc-FR.ini rename to frontend/plugins/frontend-tools/data/locale/oc-FR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pl-PL.ini b/frontend/plugins/frontend-tools/data/locale/pl-PL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pl-PL.ini rename to frontend/plugins/frontend-tools/data/locale/pl-PL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pt-BR.ini b/frontend/plugins/frontend-tools/data/locale/pt-BR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pt-BR.ini rename to frontend/plugins/frontend-tools/data/locale/pt-BR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pt-PT.ini b/frontend/plugins/frontend-tools/data/locale/pt-PT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pt-PT.ini rename to frontend/plugins/frontend-tools/data/locale/pt-PT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ro-RO.ini b/frontend/plugins/frontend-tools/data/locale/ro-RO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ro-RO.ini rename to frontend/plugins/frontend-tools/data/locale/ro-RO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ru-RU.ini b/frontend/plugins/frontend-tools/data/locale/ru-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ru-RU.ini rename to frontend/plugins/frontend-tools/data/locale/ru-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/si-LK.ini b/frontend/plugins/frontend-tools/data/locale/si-LK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/si-LK.ini rename to frontend/plugins/frontend-tools/data/locale/si-LK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sk-SK.ini b/frontend/plugins/frontend-tools/data/locale/sk-SK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sk-SK.ini rename to frontend/plugins/frontend-tools/data/locale/sk-SK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sl-SI.ini b/frontend/plugins/frontend-tools/data/locale/sl-SI.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sl-SI.ini rename to frontend/plugins/frontend-tools/data/locale/sl-SI.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sq-AL.ini b/frontend/plugins/frontend-tools/data/locale/sq-AL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sq-AL.ini rename to frontend/plugins/frontend-tools/data/locale/sq-AL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sr-CS.ini b/frontend/plugins/frontend-tools/data/locale/sr-CS.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sr-CS.ini rename to frontend/plugins/frontend-tools/data/locale/sr-CS.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sr-SP.ini b/frontend/plugins/frontend-tools/data/locale/sr-SP.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sr-SP.ini rename to frontend/plugins/frontend-tools/data/locale/sr-SP.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sv-SE.ini b/frontend/plugins/frontend-tools/data/locale/sv-SE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sv-SE.ini rename to frontend/plugins/frontend-tools/data/locale/sv-SE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/szl-PL.ini b/frontend/plugins/frontend-tools/data/locale/szl-PL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/szl-PL.ini rename to frontend/plugins/frontend-tools/data/locale/szl-PL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ta-IN.ini b/frontend/plugins/frontend-tools/data/locale/ta-IN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ta-IN.ini rename to frontend/plugins/frontend-tools/data/locale/ta-IN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/th-TH.ini b/frontend/plugins/frontend-tools/data/locale/th-TH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/th-TH.ini rename to frontend/plugins/frontend-tools/data/locale/th-TH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tl-PH.ini b/frontend/plugins/frontend-tools/data/locale/tl-PH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tl-PH.ini rename to frontend/plugins/frontend-tools/data/locale/tl-PH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tr-TR.ini b/frontend/plugins/frontend-tools/data/locale/tr-TR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tr-TR.ini rename to frontend/plugins/frontend-tools/data/locale/tr-TR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tt-RU.ini b/frontend/plugins/frontend-tools/data/locale/tt-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tt-RU.ini rename to frontend/plugins/frontend-tools/data/locale/tt-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ug-CN.ini b/frontend/plugins/frontend-tools/data/locale/ug-CN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ug-CN.ini rename to frontend/plugins/frontend-tools/data/locale/ug-CN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/uk-UA.ini b/frontend/plugins/frontend-tools/data/locale/uk-UA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/uk-UA.ini rename to frontend/plugins/frontend-tools/data/locale/uk-UA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini b/frontend/plugins/frontend-tools/data/locale/vi-VN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini rename to frontend/plugins/frontend-tools/data/locale/vi-VN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini b/frontend/plugins/frontend-tools/data/locale/zh-CN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini rename to frontend/plugins/frontend-tools/data/locale/zh-CN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/zh-TW.ini b/frontend/plugins/frontend-tools/data/locale/zh-TW.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/zh-TW.ini rename to frontend/plugins/frontend-tools/data/locale/zh-TW.ini diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source.lua b/frontend/plugins/frontend-tools/data/scripts/clock-source.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source.lua rename to frontend/plugins/frontend-tools/data/scripts/clock-source.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/dial.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/dial.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/dial.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/dial.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/hour.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/hour.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/hour.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/hour.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/minute.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/minute.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/minute.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/minute.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/second.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/second.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/second.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/second.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua b/frontend/plugins/frontend-tools/data/scripts/countdown.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua rename to frontend/plugins/frontend-tools/data/scripts/countdown.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/instant-replay.lua b/frontend/plugins/frontend-tools/data/scripts/instant-replay.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/instant-replay.lua rename to frontend/plugins/frontend-tools/data/scripts/instant-replay.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/pause-scene.lua b/frontend/plugins/frontend-tools/data/scripts/pause-scene.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/pause-scene.lua rename to frontend/plugins/frontend-tools/data/scripts/pause-scene.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/url-text.py b/frontend/plugins/frontend-tools/data/scripts/url-text.py similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/url-text.py rename to frontend/plugins/frontend-tools/data/scripts/url-text.py diff --git a/UI/frontend-plugins/frontend-tools/forms/auto-scene-switcher.ui b/frontend/plugins/frontend-tools/forms/auto-scene-switcher.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/auto-scene-switcher.ui rename to frontend/plugins/frontend-tools/forms/auto-scene-switcher.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/captions.ui b/frontend/plugins/frontend-tools/forms/captions.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/captions.ui rename to frontend/plugins/frontend-tools/forms/captions.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/output-timer.ui b/frontend/plugins/frontend-tools/forms/output-timer.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/output-timer.ui rename to frontend/plugins/frontend-tools/forms/output-timer.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/scripts.ui b/frontend/plugins/frontend-tools/forms/scripts.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/scripts.ui rename to frontend/plugins/frontend-tools/forms/scripts.ui diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in b/frontend/plugins/frontend-tools/frontend-tools-config.h.in similarity index 100% rename from UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in rename to frontend/plugins/frontend-tools/frontend-tools-config.h.in diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools.c b/frontend/plugins/frontend-tools/frontend-tools.c similarity index 100% rename from UI/frontend-plugins/frontend-tools/frontend-tools.c rename to frontend/plugins/frontend-tools/frontend-tools.c diff --git a/UI/frontend-plugins/frontend-tools/output-timer.cpp b/frontend/plugins/frontend-tools/output-timer.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/output-timer.cpp rename to frontend/plugins/frontend-tools/output-timer.cpp diff --git a/UI/frontend-plugins/frontend-tools/output-timer.hpp b/frontend/plugins/frontend-tools/output-timer.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/output-timer.hpp rename to frontend/plugins/frontend-tools/output-timer.hpp diff --git a/UI/frontend-plugins/frontend-tools/scripts.cpp b/frontend/plugins/frontend-tools/scripts.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/scripts.cpp rename to frontend/plugins/frontend-tools/scripts.cpp diff --git a/UI/frontend-plugins/frontend-tools/scripts.hpp b/frontend/plugins/frontend-tools/scripts.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/scripts.hpp rename to frontend/plugins/frontend-tools/scripts.hpp diff --git a/UI/frontend-plugins/frontend-tools/tool-helpers.hpp b/frontend/plugins/frontend-tools/tool-helpers.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/tool-helpers.hpp rename to frontend/plugins/frontend-tools/tool-helpers.hpp From 7db4a75913fb627c1d1c169dc51e5468ebb7a148 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 21:42:42 +0100 Subject: [PATCH 34/37] cmake: Update main CMakeLists file to use refactored frontend --- CMakeLists.txt | 2 +- cmake/linux/cpackconfig.cmake | 2 +- cmake/windows/cpackconfig.cmake | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a45780d1..d3fcec13b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,6 @@ add_subdirectory(plugins) add_subdirectory(test/test-input) -add_subdirectory(UI) +add_subdirectory(frontend) message_configuration() diff --git a/cmake/linux/cpackconfig.cmake b/cmake/linux/cpackconfig.cmake index f8b7869db..37eeaa035 100644 --- a/cmake/linux/cpackconfig.cmake +++ b/cmake/linux/cpackconfig.cmake @@ -5,7 +5,7 @@ include_guard(GLOBAL) include(cpackconfig_common) # Add GPLv2 license file to CPack -set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt") set(CPACK_PACKAGE_EXECUTABLES "obs") if(ENABLE_RELEASE_BUILD) diff --git a/cmake/windows/cpackconfig.cmake b/cmake/windows/cpackconfig.cmake index 2191e34dc..f51832fbd 100644 --- a/cmake/windows/cpackconfig.cmake +++ b/cmake/windows/cpackconfig.cmake @@ -5,7 +5,7 @@ include_guard(GLOBAL) include(cpackconfig_common) # Add GPLv2 license file to CPack -set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt") set(CPACK_PACKAGE_VERSION "${OBS_VERSION_CANONICAL}") set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-windows-${CMAKE_VS_PLATFORM_NAME}") set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY FALSE) From 49e23e184c6ca196ddffcf1de0905545c02fc4b8 Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Tue, 10 Sep 2024 23:43:24 +0200 Subject: [PATCH 35/37] build-aux: Replace UI directory with frontend directory for formatters --- build-aux/.run-format.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build-aux/.run-format.zsh b/build-aux/.run-format.zsh index 26c92f8f1..1596b8c72 100755 --- a/build-aux/.run-format.zsh +++ b/build-aux/.run-format.zsh @@ -54,7 +54,7 @@ invoke_formatter() { exit 2 fi - if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N)) + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N)) source_files=(${source_files:#*/(obs-websocket/deps|decklink/*/decklink-sdk|mac-syphon/syphon-framework|libdshowcapture)/*}) @@ -102,7 +102,7 @@ invoke_formatter() { fi } - if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|UI|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) + if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) source_files=(${source_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) source_files=(${source_files:#(cmake/Modules/*|*/legacy.cmake)}) @@ -150,7 +150,7 @@ invoke_formatter() { exit 2 } - if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins)/**/*.swift(.N)) + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N)) check_files() { local -i num_failures=0 From 4752be4b95616960d7135deea10903445c6228dd Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 18 Dec 2024 13:01:45 +0100 Subject: [PATCH 36/37] CI: Update actions and build scripts to use new frontend directory --- .github/actions/qt-xml-validator/action.yaml | 4 ++-- .github/scripts/.build.zsh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/qt-xml-validator/action.yaml b/.github/actions/qt-xml-validator/action.yaml index 695be4482..6539e3118 100644 --- a/.github/actions/qt-xml-validator/action.yaml +++ b/.github/actions/qt-xml-validator/action.yaml @@ -39,7 +39,7 @@ runs: uses: ./.github/actions/check-changes id: checks with: - checkGlob: 'UI/forms/**/*.ui' + checkGlob: 'frontend/forms/**/*.ui' - name: Validate XML 💯 if: fromJSON(steps.checks.outputs.hasChangedFiles) @@ -56,6 +56,6 @@ runs: if (( ${#CHANGED_FILES[@]} )); then if [[ '${{ inputs.failCondition }}' == never ]]; then set +e; fi xmllint \ - --schema ${{ github.workspace }}/UI/forms/XML-Schema-Qt5.15.xsd \ + --schema ${{ github.workspace }}/frontend/forms/XML-Schema-Qt5.15.xsd \ --noout "${CHANGED_FILES[@]}" fi diff --git a/.github/scripts/.build.zsh b/.github/scripts/.build.zsh index 2de6c29b8..f579aca3f 100755 --- a/.github/scripts/.build.zsh +++ b/.github/scripts/.build.zsh @@ -201,7 +201,7 @@ build() { rm -rf OBS.app mkdir OBS.app - ditto UI/${config}/OBS.app OBS.app + ditto frontend/${config}/OBS.app OBS.app } } popd From 1c4b60057e0782a4663f0cdec5ba476a4b3b86dc Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Wed, 8 Jan 2025 17:35:16 +0100 Subject: [PATCH 37/37] frontend: Finalize merge of OBSBasic Sources with module sources --- frontend/widgets/OBSBasic_Browser.cpp | 502 +- .../widgets/OBSBasic_SceneCollections.cpp | 9397 +------------ frontend/widgets/OBSBasic_StudioMode.cpp | 11525 +--------------- frontend/widgets/OBSBasic_Transitions.cpp | 10561 +------------- 4 files changed, 69 insertions(+), 31916 deletions(-) diff --git a/frontend/widgets/OBSBasic_Browser.cpp b/frontend/widgets/OBSBasic_Browser.cpp index ea78e9768..cfcf6b42d 100644 --- a/frontend/widgets/OBSBasic_Browser.cpp +++ b/frontend/widgets/OBSBasic_Browser.cpp @@ -1,446 +1,44 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey -#include -#include -#include -#include + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#ifdef BROWSER_AVAILABLE +#include +#include #include +#include -#include "ui_OBSExtraBrowsers.h" +#include using namespace json11; +#endif -#define OBJ_NAME_SUFFIX "_extraBrowser" +#include -enum class Column : int { - Title, - Url, - Delete, +struct QCef; +struct QCefCookieManager; - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; +#ifdef BROWSER_AVAILABLE void OBSBasic::ClearExtraBrowserDocks() { extraBrowserDockTargets.clear(); @@ -560,46 +158,8 @@ void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, con dock->setVisible(true); } } -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include "OBSBasic.hpp" - -#ifdef BROWSER_AVAILABLE -#include -#include - -#include -#include - -#include - -using namespace json11; #endif -#include - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - static std::string GenId() { std::random_device rd; diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index 488670e9d..07c16cf6b 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -15,20 +15,26 @@ along with this program. If not, see . ******************************************************************************/ +#include "OBSBasic.hpp" + +#include +#include +#include + +#include + +#include + #include #include +#include -#include -#include -#include -#include -#include -#include -#include -#include "item-widget-helpers.hpp" -#include "window-basic-main.hpp" -#include "window-importer.hpp" -#include "window-namedialog.hpp" +extern bool safe_mode; +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_virtualcam; +extern bool opt_start_replaybuffer; +extern std::string opt_starting_scene; // MARK: Constant Expressions @@ -718,612 +724,11 @@ void OBSBasic::ActivateSceneCollection(const OBSSceneCollection &collection) OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); } -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Zachary Lund - Philippe Groarke - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ -#include "ui-config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif +// MARK: - OBSBasic Scene Collection Functions using namespace std; -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) { OBSSourceAutoRelease source = obs_get_output_source(channel); @@ -1405,143 +810,6 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array return saveData; } -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - void OBSBasic::Save(const char *file) { OBSScene scene = GetCurrentScene(); @@ -1649,43 +917,6 @@ static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) } } -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - void OBSBasic::DisableRelativeCoordinates(bool enable) { /* Allow disabling relative positioning to allow loading collections @@ -1720,57 +951,6 @@ void OBSBasic::CreateDefaultScene(bool firstStart) disableSaving--; } -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) { const char *name = obs_source_get_name(filter); @@ -2208,1471 +1388,6 @@ retryScene: OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); } -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - void OBSBasic::SaveProjectNow() { if (disableSaving) @@ -3710,1686 +1425,6 @@ void OBSBasic::SaveProjectDeferred() } } -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - void OBSBasic::ClearSceneData() { disableSaving++; @@ -5490,246 +1525,6 @@ void OBSBasic::ClearSceneData() } } -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) { if (obs_missing_files_count(files) > 0) { @@ -5765,5159 +1560,3 @@ void OBSBasic::on_actionShowMissingFiles_triggered() obs_enum_all_sources(cb_sources, files); ShowMissingFilesDialog(files); } - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_StudioMode.cpp b/frontend/widgets/OBSBasic_StudioMode.cpp index 26def8786..f15c2e4c9 100644 --- a/frontend/widgets/OBSBasic_StudioMode.cpp +++ b/frontend/widgets/OBSBasic_StudioMode.cpp @@ -1,5 +1,7 @@ /****************************************************************************** Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,654 +17,16 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" + +#include +#include + #include #include -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "display-helpers.hpp" -#include "window-namedialog.hpp" -#include "menu-button.hpp" -#include "obs-hotkey.h" - -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(QuickTransition); - -static inline QString MakeQuickTransitionText(QuickTransition *qt) -{ - QString name; - - if (!qt->fadeToBlack) - name = QT_UTF8(obs_source_get_name(qt->source)); - else - name = QTStr("FadeToBlack"); - - if (!obs_transition_fixed(qt->source)) - name += QString(" (%1ms)").arg(QString::number(qt->duration)); - return name; -} - -void OBSBasic::InitDefaultTransitions() -{ - std::vector transitions; - size_t idx = 0; - const char *id; - - /* automatically add transitions that have no configuration (things - * such as cut/fade/etc) */ - while (obs_enum_transition_types(idx++, &id)) { - if (!obs_is_source_configurable(id)) { - const char *name = obs_source_get_display_name(id); - - OBSSourceAutoRelease tr = obs_source_create_private(id, name, NULL); - InitTransition(tr); - transitions.emplace_back(tr); - - if (strcmp(id, "fade_transition") == 0) - fadeTransition = tr; - else if (strcmp(id, "cut_transition") == 0) - cutTransition = tr; - } - } - - for (OBSSource &tr : transitions) { - ui->transitions->addItem(QT_UTF8(obs_source_get_name(tr)), QVariant::fromValue(OBSSource(tr))); - } -} - -void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) -{ - DStr hotkeyId; - QString hotkeyName; - - dstr_printf(hotkeyId, "OBSBasic.QuickTransition.%d", qt->id); - hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); - - auto quickTransition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - int id = (int)(uintptr_t)data; - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (pressed) - QMetaObject::invokeMethod(main, "TriggerQuickTransition", Qt::QueuedConnection, Q_ARG(int, id)); - }; - - qt->hotkey = obs_hotkey_register_frontend(hotkeyId->array, QT_TO_UTF8(hotkeyName), quickTransition, - (void *)(uintptr_t)qt->id); -} - -void QuickTransition::SourceRenamed(void *param, calldata_t *) -{ - QuickTransition *qt = reinterpret_cast(param); - - QString hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); - - obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); -} - -void OBSBasic::TriggerQuickTransition(int id) -{ - QuickTransition *qt = GetQuickTransition(id); - - if (qt && previewProgramMode) { - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (GetCurrentTransition() != qt->source) { - OverrideTransition(qt->source); - overridingTransition = true; - } - - TransitionToScene(source, false, true, qt->duration, qt->fadeToBlack); - } -} - -void OBSBasic::RemoveQuickTransitionHotkey(QuickTransition *qt) -{ - obs_hotkey_unregister(qt->hotkey); -} - -void OBSBasic::InitTransition(obs_source_t *transition) -{ - auto onTransitionStop = [](void *data, calldata_t *) { - OBSBasic *window = (OBSBasic *)data; - QMetaObject::invokeMethod(window, "TransitionStopped", Qt::QueuedConnection); - }; - - auto onTransitionFullStop = [](void *data, calldata_t *) { - OBSBasic *window = (OBSBasic *)data; - QMetaObject::invokeMethod(window, "TransitionFullyStopped", Qt::QueuedConnection); - }; - - signal_handler_t *handler = obs_source_get_signal_handler(transition); - signal_handler_connect(handler, "transition_video_stop", onTransitionStop, this); - signal_handler_connect(handler, "transition_stop", onTransitionFullStop, this); -} - -static inline OBSSource GetTransitionComboItem(QComboBox *combo, int idx) -{ - return combo->itemData(idx).value(); -} - -void OBSBasic::CreateDefaultQuickTransitions() -{ - /* non-configurable transitions are always available, so add them - * to the "default quick transitions" list */ - quickTransitions.emplace_back(cutTransition, 300, quickTransitionIdCounter++); - quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++); - quickTransitions.emplace_back(fadeTransition, 300, quickTransitionIdCounter++, true); -} - -void OBSBasic::LoadQuickTransitions(obs_data_array_t *array) -{ - size_t count = obs_data_array_count(array); - - quickTransitionIdCounter = 1; - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - OBSDataArrayAutoRelease hotkeys = obs_data_get_array(data, "hotkeys"); - const char *name = obs_data_get_string(data, "name"); - int duration = obs_data_get_int(data, "duration"); - int id = obs_data_get_int(data, "id"); - bool toBlack = obs_data_get_bool(data, "fade_to_black"); - - if (id) { - obs_source_t *source = FindTransition(name); - if (source) { - quickTransitions.emplace_back(source, duration, id, toBlack); - - if (quickTransitionIdCounter <= id) - quickTransitionIdCounter = id + 1; - - int idx = (int)quickTransitions.size() - 1; - AddQuickTransitionHotkey(&quickTransitions[idx]); - obs_hotkey_load(quickTransitions[idx].hotkey, hotkeys); - } - } - } -} - -obs_data_array_t *OBSBasic::SaveQuickTransitions() -{ - obs_data_array_t *array = obs_data_array_create(); - - for (QuickTransition &qt : quickTransitions) { - OBSDataAutoRelease data = obs_data_create(); - OBSDataArrayAutoRelease hotkeys = obs_hotkey_save(qt.hotkey); - - obs_data_set_string(data, "name", obs_source_get_name(qt.source)); - obs_data_set_int(data, "duration", qt.duration); - obs_data_set_array(data, "hotkeys", hotkeys); - obs_data_set_int(data, "id", qt.id); - obs_data_set_bool(data, "fade_to_black", qt.fadeToBlack); - - obs_data_array_push_back(array, data); - } - - return array; -} - -obs_source_t *OBSBasic::FindTransition(const char *name) -{ - for (int i = 0; i < ui->transitions->count(); i++) { - OBSSource tr = ui->transitions->itemData(i).value(); - if (!tr) - continue; - - const char *trName = obs_source_get_name(tr); - if (strcmp(trName, name) == 0) - return tr; - } - - return nullptr; -} - -void OBSBasic::TransitionToScene(OBSScene scene, bool force) -{ - obs_source_t *source = obs_scene_get_source(scene); - TransitionToScene(source, force); -} - -void OBSBasic::TransitionStopped() -{ - if (swapScenesMode) { - OBSSource scene = OBSGetStrongRef(swapScene); - if (scene) - SetCurrentScene(scene); - } - - EnableTransitionWidgets(true); - UpdatePreviewProgramIndicators(); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_STOPPED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - - swapScene = nullptr; -} - -void OBSBasic::OverrideTransition(OBSSource transition) -{ - OBSSourceAutoRelease oldTransition = obs_get_output_source(0); - - if (transition != oldTransition) { - obs_transition_swap_begin(transition, oldTransition); - obs_set_output_source(0, transition); - obs_transition_swap_end(transition, oldTransition); - } -} - -void OBSBasic::TransitionFullyStopped() -{ - if (overridingTransition) { - OverrideTransition(GetCurrentTransition()); - overridingTransition = false; - } -} - -void OBSBasic::TransitionToScene(OBSSource source, bool force, bool quickTransition, int quickDuration, bool black, - bool manual) -{ - obs_scene_t *scene = obs_scene_from_source(source); - bool usingPreviewProgram = IsPreviewProgramMode(); - if (!scene) - return; - - if (usingPreviewProgram) { - if (!tBarActive) - lastProgramScene = programScene; - programScene = OBSGetWeakRef(source); - - if (!force && !black) { - OBSSource lastScene = OBSGetStrongRef(lastProgramScene); - - if (!sceneDuplicationMode && lastScene == source) - return; - - if (swapScenesMode && lastScene && lastScene != GetCurrentSceneSource()) - swapScene = lastProgramScene; - } - } - - if (usingPreviewProgram && sceneDuplicationMode) { - scene = obs_scene_duplicate(scene, obs_source_get_name(obs_scene_get_source(scene)), - editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY - : OBS_SCENE_DUP_PRIVATE_REFS); - source = obs_scene_get_source(scene); - } - - OBSSourceAutoRelease transition = obs_get_output_source(0); - if (!transition) { - if (usingPreviewProgram && sceneDuplicationMode) - obs_scene_release(scene); - return; - } - - float t = obs_transition_get_time(transition); - bool stillTransitioning = t < 1.0f && t > 0.0f; - - // If actively transitioning, block new transitions from starting - if (usingPreviewProgram && stillTransitioning) - goto cleanup; - - if (usingPreviewProgram) { - if (!black && !manual) { - const char *sceneName = obs_source_get_name(source); - blog(LOG_INFO, "User switched Program to scene '%s'", sceneName); - - } else if (black && !prevFTBSource) { - OBSSourceAutoRelease target = obs_transition_get_active_source(transition); - const char *sceneName = obs_source_get_name(target); - blog(LOG_INFO, "User faded from scene '%s' to black", sceneName); - - } else if (black && prevFTBSource) { - const char *sceneName = obs_source_get_name(prevFTBSource); - blog(LOG_INFO, "User faded from black to scene '%s'", sceneName); - - } else if (manual) { - const char *sceneName = obs_source_get_name(source); - blog(LOG_INFO, "User started manual transition to scene '%s'", sceneName); - } - } - - if (force) { - obs_transition_set(transition, source); - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - } else { - int duration = ui->transitionDuration->value(); - - /* check for scene override */ - OBSSource trOverride = GetOverrideTransition(source); - - if (trOverride && !overridingTransition && !quickTransition) { - transition = std::move(trOverride); - duration = GetOverrideTransitionDuration(source); - OverrideTransition(transition.Get()); - overridingTransition = true; - } - - if (black && !prevFTBSource) { - prevFTBSource = source; - source = nullptr; - } else if (black && prevFTBSource) { - source = prevFTBSource; - prevFTBSource = nullptr; - } else if (!black) { - prevFTBSource = nullptr; - } - - if (quickTransition) - duration = quickDuration; - - enum obs_transition_mode mode = manual ? OBS_TRANSITION_MODE_MANUAL : OBS_TRANSITION_MODE_AUTO; - - EnableTransitionWidgets(false); - - bool success = obs_transition_start(transition, mode, duration, source); - - if (!success) - TransitionFullyStopped(); - } - -cleanup: - if (usingPreviewProgram && sceneDuplicationMode) - obs_scene_release(scene); -} - -static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr) -{ - int idx = combo->findData(QVariant::fromValue(tr)); - if (idx != -1) { - combo->blockSignals(true); - combo->setCurrentIndex(idx); - combo->blockSignals(false); - } -} - -void OBSBasic::SetTransition(OBSSource transition) -{ - OBSSourceAutoRelease oldTransition = obs_get_output_source(0); - - if (oldTransition && transition) { - obs_transition_swap_begin(transition, oldTransition); - if (transition != GetCurrentTransition()) - SetComboTransition(ui->transitions, transition); - obs_set_output_source(0, transition); - obs_transition_swap_end(transition, oldTransition); - } else { - obs_set_output_source(0, transition); - } - - bool fixed = transition ? obs_transition_fixed(transition) : false; - ui->transitionDurationLabel->setVisible(!fixed); - ui->transitionDuration->setVisible(!fixed); - - bool configurable = transition ? obs_source_configurable(transition) : false; - ui->transitionRemove->setEnabled(configurable); - ui->transitionProps->setEnabled(configurable); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_CHANGED); -} - -OBSSource OBSBasic::GetCurrentTransition() -{ - return ui->transitions->currentData().value(); -} - -void OBSBasic::on_transitions_currentIndexChanged(int) -{ - OBSSource transition = GetCurrentTransition(); - SetTransition(transition); -} - -void OBSBasic::AddTransition(const char *id) -{ - string name; - QString placeHolderText = QT_UTF8(obs_source_get_display_name(id)); - QString format = placeHolderText + " (%1)"; - obs_source_t *source = nullptr; - int i = 1; - - while ((FindTransition(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), - name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - AddTransition(id); - return; - } - - source = FindTransition(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - AddTransition(id); - return; - } - - source = obs_source_create_private(id, name.c_str(), NULL); - InitTransition(source); - ui->transitions->addItem(QT_UTF8(name.c_str()), QVariant::fromValue(OBSSource(source))); - ui->transitions->setCurrentIndex(ui->transitions->count() - 1); - CreatePropertiesWindow(source); - obs_source_release(source); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); - } -} - -void OBSBasic::on_transitionAdd_clicked() -{ - bool foundConfigurableTransitions = false; - QMenu menu(this); - size_t idx = 0; - const char *id; - - while (obs_enum_transition_types(idx++, &id)) { - if (obs_is_source_configurable(id)) { - const char *name = obs_source_get_display_name(id); - QAction *action = new QAction(name, this); - - connect(action, &QAction::triggered, [this, id]() { AddTransition(id); }); - - menu.addAction(action); - foundConfigurableTransitions = true; - } - } - - if (foundConfigurableTransitions) - menu.exec(QCursor::pos()); -} - -void OBSBasic::on_transitionRemove_clicked() -{ - OBSSource tr = GetCurrentTransition(); - - if (!tr || !obs_source_configurable(tr) || !QueryRemoveSource(tr)) - return; - - int idx = ui->transitions->findData(QVariant::fromValue(tr)); - if (idx == -1) - return; - - for (size_t i = quickTransitions.size(); i > 0; i--) { - QuickTransition &qt = quickTransitions[i - 1]; - if (qt.source == tr) { - if (qt.button) - qt.button->deleteLater(); - RemoveQuickTransitionHotkey(&qt); - quickTransitions.erase(quickTransitions.begin() + i - 1); - } - } - - ui->transitions->removeItem(idx); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); -} - -void OBSBasic::RenameTransition(OBSSource transition) -{ - string name; - QString placeHolderText = QT_UTF8(obs_source_get_name(transition)); - obs_source_t *source = nullptr; - - bool accepted = NameDialog::AskForName(this, QTStr("TransitionNameDlg.Title"), QTStr("TransitionNameDlg.Text"), - name, placeHolderText); - - if (!accepted) - return; - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - RenameTransition(transition); - return; - } - - source = FindTransition(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - RenameTransition(transition); - return; - } - - obs_source_set_name(transition, name.c_str()); - int idx = ui->transitions->findData(QVariant::fromValue(transition)); - if (idx != -1) { - ui->transitions->setItemText(idx, QT_UTF8(name.c_str())); - - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); - - ClearQuickTransitionWidgets(); - RefreshQuickTransitions(); - } -} - -void OBSBasic::on_transitionProps_clicked() -{ - OBSSource source = GetCurrentTransition(); - - if (!obs_source_configurable(source)) - return; - - auto properties = [&]() { - CreatePropertiesWindow(source); - }; - - QMenu menu(this); - - QAction *action = new QAction(QTStr("Rename"), &menu); - connect(action, &QAction::triggered, [this, source]() { RenameTransition(source); }); - menu.addAction(action); - - action = new QAction(QTStr("Properties"), &menu); - connect(action, &QAction::triggered, properties); - menu.addAction(action); - - menu.exec(QCursor::pos()); -} - -void OBSBasic::on_transitionDuration_valueChanged() -{ - OnEvent(OBS_FRONTEND_EVENT_TRANSITION_DURATION_CHANGED); -} - -QuickTransition *OBSBasic::GetQuickTransition(int id) -{ - for (QuickTransition &qt : quickTransitions) { - if (qt.id == id) - return &qt; - } - - return nullptr; -} - -int OBSBasic::GetQuickTransitionIdx(int id) -{ - for (int idx = 0; idx < (int)quickTransitions.size(); idx++) { - QuickTransition &qt = quickTransitions[idx]; - - if (qt.id == id) - return idx; - } - - return -1; -} - -void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force) -{ - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, force); -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -void OBSBasic::SetCurrentScene(OBSSource scene, bool force) -{ - if (!IsPreviewProgramMode()) { - TransitionToScene(scene, force); - } else { - OBSSource actualLastScene = OBSGetStrongRef(lastScene); - if (actualLastScene != scene) { - if (scene) - obs_source_inc_showing(scene); - if (actualLastScene) - obs_source_dec_showing(actualLastScene); - lastScene = OBSGetWeakRef(scene); - } - } - - if (obs_scene_get_source(GetCurrentScene()) != scene) { - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene itemScene = GetOBSRef(item); - obs_source_t *source = obs_scene_get_source(itemScene); - - if (source == scene) { - ui->scenes->blockSignals(true); - currentScene = itemScene.Get(); - ui->scenes->setCurrentItem(item); - ui->scenes->blockSignals(false); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - break; - } - } - } - - UpdateContextBar(true); - UpdatePreviewProgramIndicators(); - - if (scene) { - bool userSwitched = (!force && !disableSaving); - blog(LOG_INFO, "%s to scene '%s'", userSwitched ? "User switched" : "Switched", - obs_source_get_name(scene)); - } -} +#include void OBSBasic::CreateProgramDisplay() { @@ -693,12 +57,6 @@ void OBSBasic::CreateProgramDisplay() program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } -void OBSBasic::TransitionClicked() -{ - if (previewProgramMode) - TransitionToScene(GetCurrentScene()); -} - #define T_BAR_PRECISION 1024 #define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) #define T_BAR_CLAMP (T_BAR_PRECISION / 10) @@ -814,635 +172,11 @@ void OBSBasic::CreateProgramOptions() connect(configTransitions, &QAbstractButton::clicked, onConfig); } -void OBSBasic::TBarReleased() -{ - int val = tBar->value(); - - OBSSourceAutoRelease transition = obs_get_output_source(0); - - if ((tBar->maximum() - val) <= T_BAR_CLAMP) { - obs_transition_set_manual_time(transition, 1.0f); - tBar->blockSignals(true); - tBar->setValue(0); - tBar->blockSignals(false); - tBarActive = false; - EnableTransitionWidgets(true); - - OBSSourceAutoRelease target = obs_transition_get_active_source(transition); - const char *sceneName = obs_source_get_name(target); - blog(LOG_INFO, "Manual transition to scene '%s' finished", sceneName); - } else if (val <= T_BAR_CLAMP) { - obs_transition_set_manual_time(transition, 0.0f); - TransitionFullyStopped(); - tBar->blockSignals(true); - tBar->setValue(0); - tBar->blockSignals(false); - tBarActive = false; - EnableTransitionWidgets(true); - programScene = lastProgramScene; - blog(LOG_INFO, "Manual transition cancelled"); - } - - tBar->clearFocus(); -} - -static bool ValidTBarTransition(OBSSource transition) -{ - if (!transition) - return false; - - QString id = QT_UTF8(obs_source_get_id(transition)); - - if (id == "cut_transition" || id == "obs_stinger_transition") - return false; - - return true; -} - -void OBSBasic::TBarChanged(int value) -{ - OBSSourceAutoRelease transition = obs_get_output_source(0); - - if (!tBarActive) { - OBSSource sceneSource = GetCurrentSceneSource(); - OBSSource tBarTr = GetOverrideTransition(sceneSource); - - if (!ValidTBarTransition(tBarTr)) { - tBarTr = GetCurrentTransition(); - - if (!ValidTBarTransition(tBarTr)) - tBarTr = FindTransition(obs_source_get_display_name("fade_transition")); - - OverrideTransition(tBarTr); - overridingTransition = true; - - transition = std::move(tBarTr); - } - - obs_transition_set_manual_torque(transition, 8.0f, 0.05f); - TransitionToScene(sceneSource, false, false, false, 0, true); - tBarActive = true; - } - - obs_transition_set_manual_time(transition, (float)value / T_BAR_PRECISION_F); - - OnEvent(OBS_FRONTEND_EVENT_TBAR_VALUE_CHANGED); -} - -int OBSBasic::GetTbarPosition() -{ - return tBar->value(); -} - void OBSBasic::TogglePreviewProgramMode() { SetPreviewProgramMode(!IsPreviewProgramMode()); } -static inline void ResetQuickTransitionText(QuickTransition *qt) -{ - qt->button->setText(MakeQuickTransitionText(qt)); -} - -QMenu *OBSBasic::CreatePerSceneTransitionMenu() -{ - OBSSource scene = GetCurrentSceneSource(); - QMenu *menu = new QMenu(QTStr("TransitionOverride")); - QAction *action; - - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - obs_data_set_default_int(data, "transition_duration", 300); - - const char *curTransition = obs_data_get_string(data, "transition"); - int curDuration = (int)obs_data_get_int(data, "transition_duration"); - - QSpinBox *duration = new QSpinBox(menu); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(curDuration); - - auto setTransition = [this](QAction *action) { - int idx = action->property("transition_index").toInt(); - OBSSource scene = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - if (idx == -1) { - obs_data_set_string(data, "transition", ""); - return; - } - - OBSSource tr = GetTransitionComboItem(ui->transitions, idx); - - if (tr) { - const char *name = obs_source_get_name(tr); - obs_data_set_string(data, "transition", name); - } - }; - - auto setDuration = [this](int duration) { - OBSSource scene = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(scene); - - obs_data_set_int(data, "transition_duration", duration); - }; - - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); - - for (int i = -1; i < ui->transitions->count(); i++) { - const char *name = ""; - - if (i >= 0) { - OBSSource tr; - tr = GetTransitionComboItem(ui->transitions, i); - if (!tr) - continue; - name = obs_source_get_name(tr); - } - - bool match = (name && strcmp(name, curTransition) == 0); - - if (!name || !*name) - name = Str("None"); - - action = menu->addAction(QT_UTF8(name)); - action->setProperty("transition_index", i); - action->setCheckable(true); - action->setChecked(match); - - connect(action, &QAction::triggered, std::bind(setTransition, action)); - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - return menu; -} - -void OBSBasic::ShowTransitionProperties() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_transition(item, true); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::HideTransitionProperties() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_transition(item, false); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration) -{ - int64_t sceneItemId = obs_sceneitem_get_id(item); - std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - auto undo_redo = [sceneUUID, sceneItemId, show](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); - if (i) { - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - obs_sceneitem_transition_load(i, dat, show); - } - }; - - OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(item, show); - - OBSSourceAutoRelease dup = obs_source_duplicate(tr, obs_source_get_name(tr), true); - obs_sceneitem_set_transition(item, show, dup); - obs_sceneitem_set_transition_duration(item, show, duration); - - OBSDataAutoRelease transitionData = obs_sceneitem_transition_save(item, show); - - std::string undo_data(obs_data_get_json(oldTransitionData)); - std::string redo_data(obs_data_get_json(transitionData)); - if (undo_data.compare(redo_data) == 0) - return; - - QString text = show ? QTStr("Undo.ShowTransition") : QTStr("Undo.HideTransition"); - const char *name = obs_source_get_name(obs_sceneitem_get_source(item)); - undo_s.add_action(text.arg(name), undo_redo, undo_redo, undo_data, redo_data); -} - -QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) -{ - OBSSceneItem si = GetCurrentSceneItem(); - - QMenu *menu = new QMenu(QTStr(visible ? "ShowTransition" : "HideTransition")); - QAction *action; - - OBSSource curTransition = obs_sceneitem_get_transition(si, visible); - const char *curId = curTransition ? obs_source_get_id(curTransition) : nullptr; - int curDuration = (int)obs_sceneitem_get_transition_duration(si, visible); - - if (curDuration <= 0) - curDuration = obs_frontend_get_transition_duration(); - - QSpinBox *duration = new QSpinBox(menu); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(curDuration); - - auto setTransition = [this](QAction *action, bool visible) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - QString id = action->property("transition_id").toString(); - OBSSceneItem sceneItem = main->GetCurrentSceneItem(); - int64_t sceneItemId = obs_sceneitem_get_id(sceneItem); - std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(sceneItem))); - - auto undo_redo = [sceneUUID, sceneItemId, visible](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(sceneUUID.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - obs_sceneitem_t *i = obs_scene_find_sceneitem_by_id(scene, sceneItemId); - if (i) { - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - obs_sceneitem_transition_load(i, dat, visible); - } - }; - OBSDataAutoRelease oldTransitionData = obs_sceneitem_transition_save(sceneItem, visible); - if (id.isNull() || id.isEmpty()) { - obs_sceneitem_set_transition(sceneItem, visible, nullptr); - obs_sceneitem_set_transition_duration(sceneItem, visible, 0); - } else { - OBSSource tr = obs_sceneitem_get_transition(sceneItem, visible); - - if (!tr || strcmp(QT_TO_UTF8(id), obs_source_get_id(tr)) != 0) { - QString name = QT_UTF8(obs_source_get_name(obs_sceneitem_get_source(sceneItem))); - name += " "; - name += QTStr(visible ? "ShowTransition" : "HideTransition"); - tr = obs_source_create_private(QT_TO_UTF8(id), QT_TO_UTF8(name), nullptr); - obs_sceneitem_set_transition(sceneItem, visible, tr); - obs_source_release(tr); - - int duration = (int)obs_sceneitem_get_transition_duration(sceneItem, visible); - if (duration <= 0) { - duration = obs_frontend_get_transition_duration(); - obs_sceneitem_set_transition_duration(sceneItem, visible, duration); - } - } - if (obs_source_configurable(tr)) - CreatePropertiesWindow(tr); - } - OBSDataAutoRelease newTransitionData = obs_sceneitem_transition_save(sceneItem, visible); - std::string undo_data(obs_data_get_json(oldTransitionData)); - std::string redo_data(obs_data_get_json(newTransitionData)); - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr(visible ? "Undo.ShowTransition" : "Undo.HideTransition") - .arg(obs_source_get_name(obs_sceneitem_get_source(sceneItem))), - undo_redo, undo_redo, undo_data, redo_data); - }; - auto setDuration = [visible](int duration) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSceneItem item = main->GetCurrentSceneItem(); - obs_sceneitem_set_transition_duration(item, visible, duration); - }; - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, setDuration); - - action = menu->addAction(QT_UTF8(Str("None"))); - action->setProperty("transition_id", QT_UTF8("")); - action->setCheckable(true); - action->setChecked(!curId); - connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); - size_t idx = 0; - const char *id; - while (obs_enum_transition_types(idx++, &id)) { - const char *name = obs_source_get_display_name(id); - const bool match = id && curId && strcmp(id, curId) == 0; - action = menu->addAction(QT_UTF8(name)); - action->setProperty("transition_id", QT_UTF8(id)); - action->setCheckable(true); - action->setChecked(match); - connect(action, &QAction::triggered, std::bind(setTransition, action, visible)); - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - if (curId && obs_is_source_configurable(curId)) { - menu->addSeparator(); - menu->addAction(QTStr("Properties"), this, - visible ? &OBSBasic::ShowTransitionProperties : &OBSBasic::HideTransitionProperties); - } - - auto copyTransition = [this](QAction *, bool visible) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSceneItem item = main->GetCurrentSceneItem(); - obs_source_t *tr = obs_sceneitem_get_transition(item, visible); - int trDur = obs_sceneitem_get_transition_duration(item, visible); - main->copySourceTransition = obs_source_get_weak_source(tr); - main->copySourceTransitionDuration = trDur; - }; - menu->addSeparator(); - action = menu->addAction(QT_UTF8(Str("Copy"))); - action->setEnabled(curId != nullptr); - connect(action, &QAction::triggered, std::bind(copyTransition, action, visible)); - - auto pasteTransition = [this](QAction *, bool show) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSource tr = OBSGetStrongRef(main->copySourceTransition); - int trDuration = main->copySourceTransitionDuration; - if (!tr) - return; - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = main->ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - PasteShowHideTransition(item, show, tr, trDuration); - } - }; - - action = menu->addAction(QT_UTF8(Str("Paste"))); - action->setEnabled(!!OBSGetStrongRef(copySourceTransition)); - connect(action, &QAction::triggered, std::bind(pasteTransition, action, visible)); - return menu; -} - -QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt) -{ - QMenu *menu = new QMenu(parent); - QAction *action; - OBSSource tr; - - if (qt) { - action = menu->addAction(QTStr("Remove")); - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionRemoveClicked); - - menu->addSeparator(); - } - - QSpinBox *duration = new QSpinBox(menu); - if (qt) - duration->setProperty("id", qt->id); - duration->setMinimum(50); - duration->setSuffix(" ms"); - duration->setMaximum(20000); - duration->setSingleStep(50); - duration->setValue(qt ? qt->duration : 300); - - if (qt) { - connect(duration, (void(QSpinBox::*)(int)) & QSpinBox::valueChanged, this, - &OBSBasic::QuickTransitionChangeDuration); - } - - tr = fadeTransition; - - action = menu->addAction(QTStr("FadeToBlack")); - action->setProperty("fadeToBlack", true); - - if (qt) { - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); - } else { - action->setProperty("duration", QVariant::fromValue(duration)); - connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); - } - - for (int i = 0; i < ui->transitions->count(); i++) { - tr = GetTransitionComboItem(ui->transitions, i); - - if (!tr) - continue; - - action = menu->addAction(obs_source_get_name(tr)); - action->setProperty("transition_index", i); - - if (qt) { - action->setProperty("id", qt->id); - connect(action, &QAction::triggered, this, &OBSBasic::QuickTransitionChange); - } else { - action->setProperty("duration", QVariant::fromValue(duration)); - connect(action, &QAction::triggered, this, &OBSBasic::AddQuickTransition); - } - } - - QWidgetAction *durationAction = new QWidgetAction(menu); - durationAction->setDefaultWidget(duration); - - menu->addSeparator(); - menu->addAction(durationAction); - return menu; -} - -void OBSBasic::AddQuickTransitionId(int id) -{ - QuickTransition *qt = GetQuickTransition(id); - if (!qt) - return; - - /* --------------------------------- */ - - QPushButton *button = new MenuButton(); - button->setProperty("id", id); - - qt->button = button; - ResetQuickTransitionText(qt); - - /* --------------------------------- */ - - QMenu *buttonMenu = CreateTransitionMenu(button, qt); - - /* --------------------------------- */ - - button->setMenu(buttonMenu); - connect(button, &QAbstractButton::clicked, this, &OBSBasic::QuickTransitionClicked); - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - int idx = 3; - for (;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget || !widget->property("id").isValid()) - break; - } - - programLayout->insertWidget(idx, button); -} - -void OBSBasic::AddQuickTransition() -{ - int trIdx = sender()->property("transition_index").toInt(); - QSpinBox *duration = sender()->property("duration").value(); - bool fadeToBlack = sender()->property("fadeToBlack").value(); - OBSSource transition = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); - - if (!transition) - return; - - int id = quickTransitionIdCounter++; - - quickTransitions.emplace_back(transition, duration->value(), id, fadeToBlack); - AddQuickTransitionId(id); - - int idx = (int)quickTransitions.size() - 1; - AddQuickTransitionHotkey(&quickTransitions[idx]); -} - -void OBSBasic::ClearQuickTransitions() -{ - for (QuickTransition &qt : quickTransitions) - RemoveQuickTransitionHotkey(&qt); - quickTransitions.clear(); - - if (!programOptions) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget) - continue; - - int id = widget->property("id").toInt(); - if (id != 0) { - delete widget; - idx--; - } - } -} - -void OBSBasic::QuickTransitionClicked() -{ - int id = sender()->property("id").toInt(); - TriggerQuickTransition(id); -} - -void OBSBasic::QuickTransitionChange() -{ - int id = sender()->property("id").toInt(); - int trIdx = sender()->property("transition_index").toInt(); - bool fadeToBlack = sender()->property("fadeToBlack").value(); - QuickTransition *qt = GetQuickTransition(id); - - if (qt) { - OBSSource tr = fadeToBlack ? OBSSource(fadeTransition) : GetTransitionComboItem(ui->transitions, trIdx); - if (tr) { - qt->source = tr; - qt->fadeToBlack = fadeToBlack; - ResetQuickTransitionText(qt); - } - } -} - -void OBSBasic::QuickTransitionChangeDuration(int value) -{ - int id = sender()->property("id").toInt(); - QuickTransition *qt = GetQuickTransition(id); - - if (qt) { - qt->duration = value; - ResetQuickTransitionText(qt); - } -} - -void OBSBasic::QuickTransitionRemoveClicked() -{ - int id = sender()->property("id").toInt(); - int idx = GetQuickTransitionIdx(id); - if (idx == -1) - return; - - QuickTransition &qt = quickTransitions[idx]; - - if (qt.button) - qt.button->deleteLater(); - - RemoveQuickTransitionHotkey(&qt); - quickTransitions.erase(quickTransitions.begin() + idx); -} - -void OBSBasic::ClearQuickTransitionWidgets() -{ - if (!IsPreviewProgramMode()) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QWidget *widget = item->widget(); - if (!widget) - continue; - - int id = widget->property("id").toInt(); - if (id != 0) { - delete widget; - idx--; - } - } -} - -void OBSBasic::RefreshQuickTransitions() -{ - if (!IsPreviewProgramMode()) - return; - - for (QuickTransition &qt : quickTransitions) - AddQuickTransitionId(qt.id); -} - -void OBSBasic::EnableTransitionWidgets(bool enable) -{ - ui->transitions->setEnabled(enable); - - if (!enable) { - ui->transitionProps->setEnabled(false); - } else { - bool configurable = obs_source_configurable(GetCurrentTransition()); - ui->transitionProps->setEnabled(configurable); - } - - if (!IsPreviewProgramMode()) - return; - - QVBoxLayout *programLayout = reinterpret_cast(programOptions->layout()); - - for (int idx = 0;; idx++) { - QLayoutItem *item = programLayout->itemAt(idx); - if (!item) - break; - - QPushButton *button = qobject_cast(item->widget()); - if (!button) - continue; - - button->setEnabled(enable); - } - - if (transitionButton) - transitionButton->setEnabled(enable); -} - void OBSBasic::SetPreviewProgramMode(bool enabled) { if (IsPreviewProgramMode() == enabled) @@ -1594,85 +328,6 @@ void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) programY += float(PREVIEW_EDGE_SIZE); } -obs_data_array_t *OBSBasic::SaveTransitions() -{ - obs_data_array_t *transitions = obs_data_array_create(); - - for (int i = 0; i < ui->transitions->count(); i++) { - OBSSource tr = ui->transitions->itemData(i).value(); - if (!tr || !obs_source_configurable(tr)) - continue; - - OBSDataAutoRelease sourceData = obs_data_create(); - OBSDataAutoRelease settings = obs_source_get_settings(tr); - - obs_data_set_string(sourceData, "name", obs_source_get_name(tr)); - obs_data_set_string(sourceData, "id", obs_obj_get_id(tr)); - obs_data_set_obj(sourceData, "settings", settings); - - obs_data_array_push_back(transitions, sourceData); - } - - for (const OBSDataAutoRelease &transition : safeModeTransitions) { - obs_data_array_push_back(transitions, transition); - } - - return transitions; -} - -void OBSBasic::LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data) -{ - size_t count = obs_data_array_count(transitions); - - safeModeTransitions.clear(); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease item = obs_data_array_item(transitions, i); - const char *name = obs_data_get_string(item, "name"); - const char *id = obs_data_get_string(item, "id"); - OBSDataAutoRelease settings = obs_data_get_obj(item, "settings"); - - OBSSourceAutoRelease source = obs_source_create_private(id, name, settings); - if (!obs_obj_invalid(source)) { - InitTransition(source); - - ui->transitions->addItem(QT_UTF8(name), QVariant::fromValue(OBSSource(source))); - ui->transitions->setCurrentIndex(ui->transitions->count() - 1); - if (cb) - cb(private_data, source); - } else if (safe_mode || disable_3p_plugins) { - safeModeTransitions.push_back(std::move(item)); - } - } -} - -OBSSource OBSBasic::GetOverrideTransition(OBSSource source) -{ - if (!source) - return nullptr; - - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - const char *trOverrideName = obs_data_get_string(data, "transition"); - - OBSSource trOverride = nullptr; - - if (trOverrideName && *trOverrideName) - trOverride = FindTransition(trOverrideName); - - return trOverride; -} - -int OBSBasic::GetOverrideTransitionDuration(OBSSource source) -{ - if (!source) - return 300; - - OBSDataAutoRelease data = obs_source_get_private_settings(source); - obs_data_set_default_int(data, "transition_duration", 300); - - return (int)obs_data_get_int(data, "transition_duration"); -} - void OBSBasic::UpdatePreviewProgramIndicators() { bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") @@ -1697,7663 +352,12 @@ void OBSBasic::UpdatePreviewProgramIndicators() if (programLabel && programLabel->text() != program) programLabel->setText(program); } -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Zachary Lund - Philippe Groarke - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ -#include "ui-config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} OBSSource OBSBasic::GetProgramSource() { return OBSGetStrongRef(programScene); } -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - void OBSBasic::ProgramViewContextMenuRequested() { QMenu popup(this); @@ -9369,729 +373,6 @@ void OBSBasic::ProgramViewContextMenuRequested() popup.exec(QCursor::pos()); } -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - void OBSBasic::EnablePreviewProgram() { SetPreviewProgramMode(true); @@ -10102,1801 +383,13 @@ void OBSBasic::DisablePreviewProgram() SetPreviewProgramMode(false); } -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - void OBSBasic::OpenStudioProgramProjector() { int monitor = sender()->property("monitor").toInt(); OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); } -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - void OBSBasic::OpenStudioProgramWindow() { OpenProjector(nullptr, -1, ProjectorType::StudioProgram); } - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp index 26def8786..07bd796b0 100644 --- a/frontend/widgets/OBSBasic_Transitions.cpp +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -15,27 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#include +#include +#include + #include #include -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "display-helpers.hpp" -#include "window-namedialog.hpp" -#include "menu-button.hpp" -#include "obs-hotkey.h" +#include +#include -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(QuickTransition); +extern bool disable_3p_plugins; +extern bool safe_mode; static inline QString MakeQuickTransitionText(QuickTransition *qt) { @@ -99,15 +93,6 @@ void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) (void *)(uintptr_t)qt->id); } -void QuickTransition::SourceRenamed(void *param, calldata_t *) -{ - QuickTransition *qt = reinterpret_cast(param); - - QString hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); - - obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); -} - void OBSBasic::TriggerQuickTransition(int id) { QuickTransition *qt = GetQuickTransition(id); @@ -613,11 +598,6 @@ void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force) SetCurrentScene(source, force); } -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - void OBSBasic::SetCurrentScene(OBSSource scene, bool force) { if (!IsPreviewProgramMode()) { @@ -664,35 +644,6 @@ void OBSBasic::SetCurrentScene(OBSSource scene, bool force) } } -void OBSBasic::CreateProgramDisplay() -{ - program = new OBSQTDisplay(); - - program->setContextMenuPolicy(Qt::CustomContextMenu); - connect(program.data(), &QWidget::customContextMenuRequested, this, &OBSBasic::ProgramViewContextMenuRequested); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayResized, displayResize); - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderProgram, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayCreated, addDisplay); - - program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -} - void OBSBasic::TransitionClicked() { if (previewProgramMode) @@ -703,117 +654,6 @@ void OBSBasic::TransitionClicked() #define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) #define T_BAR_CLAMP (T_BAR_PRECISION / 10) -void OBSBasic::CreateProgramOptions() -{ - programOptions = new QWidget(); - QVBoxLayout *layout = new QVBoxLayout(); - layout->setSpacing(4); - - QPushButton *configTransitions = new QPushButton(); - configTransitions->setProperty("class", "icon-dots-vert"); - - QHBoxLayout *mainButtonLayout = new QHBoxLayout(); - mainButtonLayout->setSpacing(2); - - transitionButton = new QPushButton(QTStr("Transition")); - transitionButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - QHBoxLayout *quickTransitions = new QHBoxLayout(); - quickTransitions->setSpacing(2); - - QPushButton *addQuickTransition = new QPushButton(); - addQuickTransition->setProperty("class", "icon-plus"); - - QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions")); - quickTransitionsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - quickTransitions->addWidget(quickTransitionsLabel); - quickTransitions->addWidget(addQuickTransition); - - mainButtonLayout->addWidget(transitionButton); - mainButtonLayout->addWidget(configTransitions); - - tBar = new SliderIgnoreClick(Qt::Horizontal); - tBar->setMinimum(0); - tBar->setMaximum(T_BAR_PRECISION - 1); - - tBar->setProperty("class", "slider-tbar"); - - connect(tBar, &QSlider::valueChanged, this, &OBSBasic::TBarChanged); - connect(tBar, &QSlider::sliderReleased, this, &OBSBasic::TBarReleased); - - layout->addStretch(0); - layout->addLayout(mainButtonLayout); - layout->addLayout(quickTransitions); - layout->addWidget(tBar); - layout->addStretch(0); - - programOptions->setLayout(layout); - - auto onAdd = [this]() { - QScopedPointer menu(CreateTransitionMenu(this, nullptr)); - menu->exec(QCursor::pos()); - }; - - auto onConfig = [this]() { - QMenu menu(this); - QAction *action; - - auto toggleEditProperties = [this]() { - editPropertiesMode = !editPropertiesMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto toggleSwapScenesMode = [this]() { - swapScenesMode = !swapScenesMode; - }; - - auto toggleSceneDuplication = [this]() { - sceneDuplicationMode = !sceneDuplicationMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto showToolTip = [&]() { - QAction *act = menu.activeAction(); - QToolTip::showText(QCursor::pos(), act->toolTip(), &menu, menu.actionGeometry(act)); - }; - - action = menu.addAction(QTStr("QuickTransitions.DuplicateScene")); - action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT")); - action->setCheckable(true); - action->setChecked(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleSceneDuplication); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.EditProperties")); - action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT")); - action->setCheckable(true); - action->setChecked(editPropertiesMode); - action->setEnabled(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleEditProperties); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.SwapScenes")); - action->setToolTip(QTStr("QuickTransitions.SwapScenesTT")); - action->setCheckable(true); - action->setChecked(swapScenesMode); - connect(action, &QAction::triggered, toggleSwapScenesMode); - connect(action, &QAction::hovered, showToolTip); - - menu.exec(QCursor::pos()); - }; - - connect(transitionButton.data(), &QAbstractButton::clicked, this, &OBSBasic::TransitionClicked); - connect(addQuickTransition, &QAbstractButton::clicked, onAdd); - connect(configTransitions, &QAbstractButton::clicked, onConfig); -} - void OBSBasic::TBarReleased() { int val = tBar->value(); @@ -894,11 +734,6 @@ int OBSBasic::GetTbarPosition() return tBar->value(); } -void OBSBasic::TogglePreviewProgramMode() -{ - SetPreviewProgramMode(!IsPreviewProgramMode()); -} - static inline void ResetQuickTransitionText(QuickTransition *qt) { qt->button->setText(MakeQuickTransitionText(qt)); @@ -1443,157 +1278,6 @@ void OBSBasic::EnableTransitionWidgets(bool enable) transitionButton->setEnabled(enable); } -void OBSBasic::SetPreviewProgramMode(bool enabled) -{ - if (IsPreviewProgramMode() == enabled) - return; - - os_atomic_set_bool(&previewProgramMode, enabled); - emit PreviewProgramModeChanged(enabled); - - if (IsPreviewProgramMode()) { - if (!previewEnabled) - EnablePreviewDisplay(true); - - CreateProgramDisplay(); - CreateProgramOptions(); - - OBSScene curScene = GetCurrentScene(); - - OBSSceneAutoRelease dup; - if (sceneDuplicationMode) { - dup = obs_scene_duplicate(curScene, obs_source_get_name(obs_scene_get_source(curScene)), - editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY - : OBS_SCENE_DUP_PRIVATE_REFS); - } else { - dup = std::move(OBSScene(curScene)); - } - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *dup_source = obs_scene_get_source(dup); - obs_transition_set(transition, dup_source); - - if (curScene) { - obs_source_t *source = obs_scene_get_source(curScene); - obs_source_inc_showing(source); - lastScene = OBSGetWeakRef(source); - programScene = OBSGetWeakRef(source); - } - - RefreshQuickTransitions(); - - programLabel = new QLabel(QTStr("StudioMode.ProgramSceneLabel"), this); - programLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); - programLabel->setProperty("class", "label-preview-title"); - - programWidget = new QWidget(); - programLayout = new QVBoxLayout(); - - programLayout->setContentsMargins(0, 0, 0, 0); - programLayout->setSpacing(0); - - programLayout->addWidget(programLabel); - programLayout->addWidget(program); - - programWidget->setLayout(programLayout); - - ui->previewLayout->addWidget(programOptions); - ui->previewLayout->addWidget(programWidget); - ui->previewLayout->setAlignment(programOptions, Qt::AlignCenter); - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED); - - blog(LOG_INFO, "Switched to Preview/Program mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } else { - OBSSource actualProgramScene = OBSGetStrongRef(programScene); - if (!actualProgramScene) - actualProgramScene = GetCurrentSceneSource(); - else - SetCurrentScene(actualProgramScene, true); - TransitionToScene(actualProgramScene, true); - - delete programOptions; - delete program; - delete programLabel; - delete programWidget; - - if (lastScene) { - OBSSource actualLastScene = OBSGetStrongRef(lastScene); - if (actualLastScene) - obs_source_dec_showing(actualLastScene); - lastScene = nullptr; - } - - programScene = nullptr; - swapScene = nullptr; - prevFTBSource = nullptr; - - for (QuickTransition &qt : quickTransitions) - qt.button = nullptr; - - if (!previewEnabled) - EnablePreviewDisplay(false); - - ui->transitions->setEnabled(true); - tBarActive = false; - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED); - - blog(LOG_INFO, "Switched to regular Preview mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } - - ResetUI(); - UpdateTitleBar(); -} - -void OBSBasic::RenderProgram(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderProgram"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->programCX = int(window->programScale * float(ovi.base_width)); - window->programCY = int(window->programScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->programX, window->programY, window->programCX, window->programCY); - - obs_render_main_texture_src_color_only(); - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - - /* resize program panel to fix to the top section of the window */ - targetSize = GetPixelSize(program); - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, programX, programY, programScale); - - programX += float(PREVIEW_EDGE_SIZE); - programY += float(PREVIEW_EDGE_SIZE); -} - obs_data_array_t *OBSBasic::SaveTransitions() { obs_data_array_t *transitions = obs_data_array_create(); @@ -1673,10230 +1357,7 @@ int OBSBasic::GetOverrideTransitionDuration(OBSSource source) return (int)obs_data_get_int(data, "transition_duration"); } -void OBSBasic::UpdatePreviewProgramIndicators() -{ - bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") - : false; - - ui->previewLabel->setVisible(labels); - - if (programLabel) - programLabel->setVisible(labels); - - if (!labels) - return; - - QString preview = - QTStr("StudioMode.PreviewSceneName").arg(QT_UTF8(obs_source_get_name(GetCurrentSceneSource()))); - - QString program = QTStr("StudioMode.ProgramSceneName").arg(QT_UTF8(obs_source_get_name(GetProgramSource()))); - - if (ui->previewLabel->text() != preview) - ui->previewLabel->setText(preview); - - if (programLabel && programLabel->text() != program) - programLabel->setText(program); -} -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Zachary Lund - Philippe Groarke - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ -#include "ui-config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - 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.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * 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; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - int OBSBasic::GetTransitionDuration() { return ui->transitionDuration->value(); } - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* 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(); - 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; - } - -#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(); - return; - } -#endif - - 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 (button == QMessageBox::No) { - event->ignore(); - restart = false; - 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); - - 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; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings 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::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -}